✨ Publier v1 is live — a polished docs platform built for the open web.
Skip to content

Tabs & CodeGroup

Tabbed panels and tabbed code blocks — ARIA-compliant keyboard nav plus cross-page sync via localStorage. CodeGroup is tab-for-code; Tabs is tab-for-anything.

Two tab flavors, same UX engine:

  • <CodeGroup> — built for code. Pass an array of { label, lang, code } items.
  • <Tabs> — built for anything else: text, tables, mixed MDX. Each tab’s content is a JSX node.

Both support keyboard navigation (ArrowLeft/Right, Home/End) and cross-instance sync via syncKey.

CodeGroup

const greet = (name: string) => `Hello, ${name}!`;
greet('world');
import { CodeGroup } from '@publier/shell/components';
<CodeGroup items={[
{ label: 'TypeScript', lang: 'ts', code: "const greet = (name: string) => `Hello, ${name}!`;" },
{ label: 'JavaScript', lang: 'js', code: "const greet = (name) => `Hello, ${name}!`;" },
{ label: 'Python', lang: 'python', code: "def greet(name):\n return f'Hello, {name}!'" },
{ label: 'Go', lang: 'go', code: 'func greet(name string) string {\n return fmt.Sprintf("Hello, %s!", name)\n}' },
]} />

With a filename header

terminal
pnpm add @publier/shell

For a themed pnpm add block, use <PackageInstall> — it picks up daisyUI tokens so it stays consistent with the surrounding theme.

Cross-page sync

When two <CodeGroup> instances share a syncKey, switching the active tab in one switches every other instance on the page (and persists across navigations):

import { Aside } from '@publier/shell/components';
<Aside type='note'>Both groups stay in sync.</Aside>
<CodeGroup syncKey="lang" items={[
{ label: 'TypeScript', lang: 'ts', code: "import { Aside } from '…';" },
{ label: 'JavaScript', lang: 'js', code: "const { Aside } = require('…');" },
]} />
<CodeGroup syncKey="lang" items={[
{ label: 'TypeScript', lang: 'ts', code: "<Aside type='note'>Sync works across instances.</Aside>" },
{ label: 'JavaScript', lang: 'js', code: "<Aside type='note'>Sync works across instances.</Aside>" },
]} />

Try clicking a tab above — the second group flips to match.

Tabs (rich content)

Use <Tabs> when panels hold more than code — platform instructions, comparison tables, mixed prose. Labels are prop-defined; each panel is a child with slot="tab-N" (index-aligned with labels):

Upload dist/ after a successful build:

publier build && rsync -av dist/ host:/var/www/site/
import { Tabs } from '@publier/shell/components';
<Tabs labels={['Static host', 'Docker', 'CI']}>
<div slot="tab-0">
<p>Upload <code>dist/</code> after a successful build:</p>
<pre><code>publier build && rsync -av dist/ host:/var/www/site/</code></pre>
</div>
<div slot="tab-1">
<p>Serve with the official nginx image:</p>
<pre><code>FROM nginx:alpine{'\n'}COPY dist/ /usr/share/nginx/html/</code></pre>
</div>
<div slot="tab-2">
<p>From a GitHub Actions step:</p>
<pre><code>- run: pnpm install --frozen-lockfile{'\n'}- run: publier build</code></pre>
</div>
</Tabs>

Panels go in named slots (tab-0, tab-1, …) so each panel stays as plain HTML in the DOM from SSR onward. Every panel is always rendered (inactive ones carry the native hidden attribute), which keeps them SEO-indexable and find-in-page-searchable.

Props

<CodeGroup>

PropTypeDefaultDescription
itemsCodeGroupItem[]Array of code panels. Required.
syncKeystringSync the active tab across instances sharing the key. Persisted to localStorage.

CodeGroupItem

FieldTypeDescription
labelstringTab label. Required. Also the sync identifier.
codestringCode string. Required.
langstringLanguage identifier for syntax highlighting.
filenamestringOptional filename header above the code.

<Tabs>

PropTypeDefaultDescription
labelsstring[]Tab labels, index-aligned with the tab-N panel slots. Required. Also the sync identifier when syncKey is set.
defaultIndexnumber0Index of the initially active tab.
syncKeystringCross-instance sync key.
classstringExtra CSS class on the outer wrapper.

Each panel is a child element (or <Fragment>) with slot="tab-N", where N is the zero-based index into labels.

Behaviour

  • Follows the ARIA Authoring Practices Guiderole="tablist", role="tab", role="tabpanel".
  • Keyboard navigation: ArrowLeft/ArrowRight cycles tabs (wraps), Home/End jumps to first/last.
  • syncKey persists the active-tab label to localStorage['publier-tabs-sync:{syncKey}']. All instances sharing a key update together on the same page and across page loads.
  • Inactive panels use the native hidden attribute; every panel is in the DOM (good for SEO and find-in-page).