Esc

comark-tabs

Tabs

Tabs of any content, code blocks grouped by package manager or language, and npm commands in every package manager, that switch together across the page.

Playground
macOS
Press ⌘K to find the pigeon data.
Windows
Press Ctrl+K to find the pigeon data.

Without a script, every tab shows under its label, one after another. The script adds:

  • The tab strip, whose highlight slides to the tab you pick.
  • Arrow keys to move between tabs.
  • sync, which switches every block with the same key at once and remembers the choice.

Each :::tabs-item inside ::tabs is a tab, named by its label.

Playground
This week
14 rubber ducks bought, 14 bugs fixed.
Last week
3 rubber ducks bought, 3 bugs fixed.
Prop
sync A key shared by blocks that switch together. Picking a tab picks the tab with the same label in every block with that key, now and on the next visit.

One tab. It holds any Markdown, even other tabs.

Prop
label The tab's name, required. Labels in one block must differ.
icon An Iconify name such as lucide:apple, or an image URL, drawn by comark-icon before the label. See Icons.

Code blocks as tabs, each named by the label in brackets after its language. The label becomes the tab, so the code block doesn't repeat it, and the tabs sit in a header joined to the block. Without a script, each block keeps its label as a header of its own.

Playground
pnpm
pnpm add comark-tabs
npm
npm install comark-tabs
Prop
sync As on ::tabs. A code group and tabs with the same key switch together.

A code block takes an icon after its label, as icon=logos:pnpm.

A code block with npm2yarn after its language holds npm commands and becomes a code group with a tab for each package manager: npm, pnpm, yarn, and bun. It has sync="pm", so it switches with the code groups you write with that key.

Playground
npm
npm install -D comark-tabs
npx nuxi@latest module add ui
pnpm
pnpm add -D comark-tabs
pnpm dlx nuxi@latest module add ui
yarn
yarn add --dev comark-tabs
yarn dlx nuxi@latest module add ui
bun
bun add --dev comark-tabs
bunx nuxi@latest module add ui

Only lines that start with npm or npx, or commands after &&, ||, or ;, change, so cd my-npm-app stays as written. npm-to-yarn converts them, and bun's tab writes bunx and bun create as Bun's docs do. In a ::code-group the block is one tab already, and npm2yarn does nothing. Set the managers with the managers option.

The tabs follow the WAI-ARIA tabs pattern. Tab moves focus into the strip and on to the open panel. The left and right arrow keys move between tabs and open them. They wrap at the ends and follow the reading direction. Home and End go to the first and last tab.

Blocks with the same sync switch together by label, so ::code-group{sync="pm"} on every install snippet keeps the reader's package manager. The script saves the choice in localStorage under comark-tabs: and the key, and restores it on the next page that uses it.

When a switch changes the height of blocks above the one the reader clicked, the page scrolls by as much, so the tab stays under the pointer.

A code block marked npm2yarn gets a tab for npm, pnpm, yarn, and bun, in that order. Set others, in the order you want, with managers; Deno is there to add:

tabs({ managers: ["pnpm", "npm", "yarn", "bun", "deno"] });
ts

A command a manager has no form for, such as npm ci in pnpm, leaves that manager's tab out, keeps npm's, and adds an issue. Write that block as a ::code-group{sync="pm"} by hand. Register comark-tabs before Shiki, so Shiki highlights every tab it adds.

Give a tab an icon, either an Iconify name, such as lucide:apple or logos:pnpm, or the URL of an image. comark-icon resolves it while parsing and draws it. The icon shows before the label in the strip, shows without the script, and looks and sizes like an :icon.

Playground
macOS
Press ⌘K to search.
Windows
Press Ctrl+K to search.

Set icons once for every block with icons, by label, for tabs that don't set their own. Icons come from the Iconify API by default. Pass the resolver the page's :icon uses as iconResolver, so they share its sets and fetch:

import { icons as logos } from "@iconify-json/logos";
import icon, { createIcons } from "comark-icon/html";
import tabs from "comark-tabs/html";

const icons = createIcons({ collections: { logos } });
const plugins = [
  icon(icons),
  tabs({ iconResolver: icons, icons: { pnpm: "logos:pnpm", npm: "logos:npm-icon" } }),
];
ts

Size them with --comark-icon-size, and style them as comark-icon's theme describes.

The plugin adds a problem with any block to meta.issues, as Validation describes:

`::code-group` 1, block 2: a code block needs a label. Put it in brackets after the language: ```bash [npm].
txt

It reports:

  • A tab without a label, or two tabs with the same label.
  • An npm2yarn command a package manager has no form for, or an npm2yarn block with no npm command.
  • An icon it can't draw, in comark-icon's words.
  • Content outside every tab, which doesn't render.
  • Anything but code blocks in a code group.
  • A block with no tabs.
  • A :::tabs-item outside ::tabs, which renders as plain content.
  • A prop the block doesn't read.

Each one has its own rule in Doctor. Rendering never throws.

Images in tabs zoom with comark-zoom like any other image. A gallery or zoom group only pages through what's shown, so images in closed tabs stay out of it.

The options go under tabs in comarkUi(), or to tabs() when you use it alone.

Option Default
managers ["npm", "pnpm", "yarn", "bun"] The tabs an npm2yarn code block becomes, in order. Add "deno" for Deno. See Package managers.
icons {} Icons for tabs that don't set one, by label, such as { pnpm: "logos:pnpm" }. Labels match without regard to case. Each value is an Iconify name or an image URL, like the icon prop. See Icons.
iconResolver The Iconify API Resolves the icons, from createIcons() in comark-icon. Pass the one the page's other plugins use, so they share its collections and fetch. comark-ui passes the one its icon uses.

comark-ui brings comark-tabs and registers it. To use it alone, install it and pass the renderer's components:

pnpm add comark @comark/html comark-tabs
import { createHtmlRenderer } from "@comark/html";
import tabs, { components } from "comark-tabs/html";

const render = createHtmlRenderer({ plugins: [tabs()], components });
ts

For Vue, React, or Svelte, import from comark-tabs/vue, comark-tabs/react, or comark-tabs/svelte. Svelte needs 5.20 or later. Renderers covers server rendering and using it with other plugins.

The tab strip is a segmented control in the style of shadcn/ui, and follows the shared tokens in Theming. Five variables of its own change the strip, the open tab, and the space under them. The rules sit in a comark CSS layer, so a plain rule of your own wins over any of them.

Token Used for
--comark-background The open tab
--comark-foreground The open tab's label, and hovered labels
--comark-muted The other labels, and the captions without the script
--comark-border The strip's frame
--comark-accent The strip's tint
--comark-ring Focus rings on tabs and panels
--comark-radius The strip's, tabs', and panels' corners
--comark-font The labels

Set these on .ct or any ancestor, such as :root:

Variable Default Used for
--ct-surface --comark-accent at 50% Behind the strip, and a code group's captions without the script
--ct-active --comark-background, a little lighter when it is dark The open tab
--ct-active-text --comark-foreground The open tab's label
--ct-gap 0.75rem The space between the strip and the open panel, except in a code group, whose block hangs from its strip
--ct-font-size 0.8125rem The labels and captions

Icons in tabs come from comark-icon: size them with --comark-icon-size, as its theming describes.

Selector Is
.ct The block, with data-tabs-code when it's a code group and data-tabs-sync when it has a sync key
.ct-list The strip
.ct-tab Each tab, with aria-selected="true" on the open one
.ct-panel Each panel, with data-inactive while it's closed
.ct-caption The label over each panel, shown only without the script
[data-tabs-ready] On <html> once the script runs

One rule sits outside the comark layer: pages style code blocks with plain rules, so the rule that drops a code group's block margin and top corners, to join it to its strip, has to beat them.

An open tab in the page's brand color:

:root {
  --ct-active: oklch(0.55 0.2 260);
  --ct-active-text: white;
}
css

To try it, paste it into Extra CSS in the playground's Theme pane.