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’scontentis 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');const greet = (name) => `Hello, ${name}!`;greet('world');def greet(name): return f'Hello, {name}!'
greet('world')func greet(name string) string { return fmt.Sprintf("Hello, %s!", name)}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
pnpm add @publier/shellpublier devpublier buildFor 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';const { Aside } = require('@publier/shell/components');<Aside type='note'>Both groups stay in sync.</Aside><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/Serve with the official nginx image:
FROM nginx:alpine
COPY dist/ /usr/share/nginx/html/From a GitHub Actions step:
- run: pnpm install —frozen-lockfile
- run: publier buildimport { 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 nativehiddenattribute), which keeps them SEO-indexable and find-in-page-searchable.
Props
<CodeGroup>
| Prop | Type | Default | Description |
|---|---|---|---|
items | CodeGroupItem[] | — | Array of code panels. Required. |
syncKey | string | — | Sync the active tab across instances sharing the key. Persisted to localStorage. |
CodeGroupItem
| Field | Type | Description |
|---|---|---|
label | string | Tab label. Required. Also the sync identifier. |
code | string | Code string. Required. |
lang | string | Language identifier for syntax highlighting. |
filename | string | Optional filename header above the code. |
<Tabs>
| Prop | Type | Default | Description |
|---|---|---|---|
labels | string[] | — | Tab labels, index-aligned with the tab-N panel slots. Required. Also the sync identifier when syncKey is set. |
defaultIndex | number | 0 | Index of the initially active tab. |
syncKey | string | — | Cross-instance sync key. |
class | string | — | Extra 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 Guide —
role="tablist",role="tab",role="tabpanel". - Keyboard navigation: ArrowLeft/ArrowRight cycles tabs (wraps), Home/End jumps to first/last.
syncKeypersists the active-tab label tolocalStorage['publier-tabs-sync:{syncKey}']. All instances sharing a key update together on the same page and across page loads.- Inactive panels use the native
hiddenattribute; every panel is in the DOM (good for SEO and find-in-page).
Related
- Install-command tabs → PackageInstall.
- Individual code blocks → Code blocks.