Tabs
ImplementedHorizontal tab group with underline indicator. WAI-ARIA tabs pattern: role='tablist', roving tabindex, ArrowLeft/Right to navigate. variant='href' renders the former TabsTrigger link-mode strip (anchor or button tabs, underline or pills appearance) — TabsTrigger was merged into Tabs (Wave 3 Tier-1, L3-M3).
patterns/molecules/tabs.htmlTab strip with two rendering modes: state (default — WAI-ARIA tabs pattern with an inline tabpanel) and href (link-driven strip, the former TabsTrigger molecule).
Tabs
molecules
State mode (default): button-driven tabs with roving tabindex, ArrowLeft/ArrowRight/Home/End navigation, focus-follows-selection, and an inline tabpanel for the active item; keyboard navigation skips disabled tabs. Href mode (link-driven, no keyboard nav): items carrying an href render anchors, items without one render buttons that call onChange — there are no arrow keys, no roving tabindex, and no tabpanel in this mode. The appearance prop selects the visual treatment in both variants: "underline" (tab bar with a bottom rule) or "pills" (prominent filled chips).
state mode (default) — button tabs + tabpanel
Line-up, venue rules, and accessibility info for the show.
state mode + appearance="pills" — prominent treatment, same keyboard semantics
Line-up, venue rules, and accessibility info for the show.
href mode (link-driven, no keyboard nav) — no tabpanel
Usage
Tabs is a controlled component — pair activeId with local state and wire onChange to it. Each item's panel renders inside the active tabpanel. Keyboard navigation (ArrowLeft/ArrowRight, Home/End) and roving tabindex are built in for state mode only — the default variant="state".
variant="href" is link-driven — no arrows, no tabpanel, no roving tabindex. Each href item is a plain anchor in the normal tab order; each href-less item is a button that calls onChange. Use it for URL-driven sub-navigation, not for swapping in-page content panels.
import { Tabs } from '@epasslive/design-system'
// state mode (default) — controlled, with an inline tabpanel
const [activeId, setActiveId] = useState('overview')
<Tabs
ariaLabel="Event information"
activeId={activeId}
onChange={setActiveId}
items={[
{ id: 'overview', label: 'Overview', panel: <p>Line-up and venue info.</p> },
{ id: 'tickets', label: 'Tickets', badge: 3, panel: <p>3 tiers on sale.</p> },
]}
/>
// href mode — link-driven strip (former TabsTrigger), no tabpanel
<Tabs
variant="href"
appearance="pills"
ariaLabel="Event sections"
activeId={activeId}
onChange={setActiveId}
items={[
{ id: 'overview', label: 'Overview', href: '/events/42' },
{ id: 'schedule', label: 'Schedule', href: '/events/42/schedule' },
{ id: 'merch', label: 'Merch' }, // no href → button, calls onChange
]}
/>Props
items*TabItem[]Tab items — id, label, optional badge/panel/href/disabled.
activeId*stringId of the active tab.
onChange*(id: string) => voidCalled when a tab is activated (button items only in href mode).
ariaLabel*stringAccessible name for the tablist.
variant"state" | "href""state"Button-driven tabs vs link-driven strip.
appearance"underline" | "pills""underline"Visual treatment (both variants): underline tab-bar, or prominent filled pills.
classNamestringAdditional CSS classes.
idstringStable id prefix for tabs and panels.
Props
Tabs
itemsTabItem[]—2-6 tabs
activeIdstring—onChange(id: string) => void—variant'state' | 'href''state'state: button tabs with keyboard nav + inline tabpanel; href: link-mode strip (former TabsTrigger)
appearance'underline' | 'pills''underline'visual treatment (both variants): underline tab-bar, or prominent filled pills
ariaLabelstring—Synced from source — no interface JSDoc yet; document on `Tabs.tsx`.
<Tabs />Accessibility
Type
1 ruleARIA
1 ruleUsage
Import
import { Tabs } from "@epasslive/design-system"Example 1
<Tabs />Example 2
<Tabs variant="default" />Anti-patterns
— Do not hardcode data in molecules
— Avoid complex business logic in molecules