Primitive

Tabs

Accessible tablist primitive. Keyboard nav (Arrow / Home / End / Enter / Space), view-transition safe, and supports hub-tinted accent colors.

Default (underline)

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

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")

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")

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 gets role="tab" and aria-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>