Primitive
Tabs
Accessible tablist primitive. Keyboard nav (Arrow / Home / End / Enter / Space), view-transition safe, and supports hub-tinted accent colors.
Default (underline)
The default underline variant works well inside articles and dense content areas.
Pass
items and slot panels by id.
Roving tabindex + ArrowLeft/Right cycles. Home / End jump. Try it.
usage.astro | astro
<Tabs
items={[
{ id: 'overview', label: 'Overview' },
{ id: 'usage', label: 'Usage' },
{ id: 'a11y', label: 'Accessibility' },
]}
defaultId="overview"
>
<div data-tab-panel="overview">Overview panel content</div>
<div data-tab-panel="usage">Usage panel content</div>
<div data-tab-panel="a11y">Accessibility panel content</div>
</Tabs> Pill variant
Daily metrics panel.
Weekly metrics panel.
Monthly metrics panel.
usage.astro | astro
<Tabs
items={[
{ id: 'd', label: 'Daily' },
{ id: 'w', label: 'Weekly' },
{ id: 'm', label: 'Monthly' },
]}
defaultId="w"
variant="pill"
>
<div data-tab-panel="d">Daily metrics panel.</div>
<div data-tab-panel="w">Weekly metrics panel.</div>
<div data-tab-panel="m">Monthly metrics panel.</div>
</Tabs> Hub-tinted (category="compliance")
EU AI Act, GDPR, NIST AI RMF...
Article 10 audit-readiness checklists.
High-risk classification rubric.
usage.astro | astro
<Tabs
items={[
{ id: 'reg', label: 'Regulation' },
{ id: 'audit', label: 'Audit' },
{ id: 'risk', label: 'Risk' },
]}
defaultId="reg"
category="compliance"
>
<div data-tab-panel="reg">EU AI Act, GDPR, NIST AI RMFโฆ</div>
<div data-tab-panel="audit">Audit-readiness checklists.</div>
<div data-tab-panel="risk">High-risk classification rubric.</div>
</Tabs> Hub-tinted pill (category="research")
Curated paper digest.
Open-weights benchmark scoreboard.
usage.astro | astro
<Tabs
items={[
{ id: 'p', label: 'Papers' },
{ id: 'b', label: 'Benchmarks' },
]}
defaultId="p"
variant="pill"
category="research"
>
<div data-tab-panel="p">Curated paper digest.</div>
<div data-tab-panel="b">Open-weights benchmark scoreboard.</div>
</Tabs> Anatomy
| Property | Behavior |
|---|---|
| Tablist | Outer container with `role="tablist"`. Holds the trigger row. |
| Tab | Each clickable trigger โ `role="tab"`, with `aria-selected` toggled. |
| Indicator | Underline (variant="underline") or filled pill background (variant="pill"). |
| Panel | One `<div data-tab-panel="...">` per tab โ only the active panel renders. |
Accessibility
- Implements the W3C APG Tabs pattern exactly.
- Tablist exposes
role="tablist"; each trigger getsrole="tab"andaria-selected. - Roving
tabindex: only the active tab is in the tab order. ArrowLeft / ArrowRight cycles, Home / End jump to extremes. - Activating a tab focuses the panel via
aria-controlsโrole="tabpanel". - Triggers are real
<button>elements โ Enter / Space activate, no JS-only click handlers. - Focus ring (
--ds-shadow-focus-ring) appears on keyboard nav but is suppressed on mouse click for visual calm.
Do / Don't
| Property | Do | Don't |
|---|---|---|
| Label length | Keep tab labels under ~16 characters; pick the shortest noun phrase that survives translation. | Use full sentences or icon-only labels without a tooltip. |
| Tab count | Cap at 4-5 visible tabs per surface. | Stuff 9 tabs onto a tablist โ switch to a Select if the count keeps growing. |
| Content discovery | Pre-render tab panels in the DOM so search engines and reader-mode see all content. | Lazy-render panel HTML if SEO matters. |
| Single-panel UI | Use Tabs when each panel really is a peer view of the same data. | Use Tabs to fake routing โ pick a Side-nav + URL state instead. |
| Hub tinting | Pass `category` to tint the active tab with the hub accent. | Hard-code a single hub color in CSS โ breaks hub-tinted identity. |
Props
| Property | Type | Default | Notes |
|---|---|---|---|
| items | Array<{id, label}> | required | - |
| defaultId | string | first item.id | Which tab is active on mount |
| variant | "underline" | "pill" | "underline" | Visual style |
| category | HubSlug | - | Hub-tinted accent (e.g. "compliance") |
| ariaLabel | string | "Tabs" | Accessible label for the tablist |
| className | string | "" | Extra wrapper classes |
Source
Tabs.astro usage | astro
<Tabs
items={[
{ id: 'overview', label: 'Overview' },
{ id: 'usage', label: 'Usage' },
]}
defaultId="overview"
variant="underline"
category="compliance"
>
<div data-tab-panel="overview">Panel A</div>
<div data-tab-panel="usage">Panel B</div>
</Tabs>