Tabs
since v0.2.0Accessible tab container root — manages active tab state and keyboard navigation.
Wraps @radix-ui/react-tabs (per docs/adr/0001-headless-primitive-baseline.md). Radix owns:
- Keyboard navigation (Arrow keys, Home/End, looping)
- Roving tabindex + automatic activation on focus
- ARIA wiring (
role="tablist"/tab/tabpanel,aria-selected,
aria-controls, aria-labelledby)
- Skipping disabled tabs in keyboard navigation
We own the public API (value / defaultValue / onValueChange) and the
visual treatment of the strip and triggers.
Install
Add the package and import the component.
pnpm add @hey-mike/tungstenpnpm add @hey-mike/tungstenimport { Tabs } from '@hey-mike/tungsten';import { Tabs } from '@hey-mike/tungsten';Preview
Same fixtures used by the visual-regression suite.
Usage
apps/docs/app/snapshots/tabs/page.tsx
import { Tabs, TabsList, TabsTrigger, TabsPanel } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
import { HeroSpecimen } from '../_components/HeroSpecimen';
export default function TabsSnapshot() {
return (
<VariantGrid
title="Tabs"
hero={
<HeroSpecimen>
<Tabs defaultValue="results">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
</TabsList>
<TabsPanel value="overview" className="pt-3">
<p className="text-ink-2 text-xs">Overview panel.</p>
</TabsPanel>
<TabsPanel value="results" className="pt-3">
<p className="text-ink-1 text-sm font-medium">38 / 42 passed</p>
</TabsPanel>
</Tabs>
</HeroSpecimen>
}
variants={[
{
label: 'default',
node: (
<Tabs defaultValue="results">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
</TabsList>
<TabsPanel value="overview" className="pt-4 space-y-2">
<p className="text-ink-1 text-sm font-medium">Response Quality</p>
<p className="text-ink-2 text-sm leading-relaxed">
Evaluates clarity, depth, and factual correctness of model output across 42 test cases.
</p>
</TabsPanel>
<TabsPanel value="results" className="pt-4 space-y-2">
<p className="text-ink-3 font-mono text-2xs uppercase tracking-label">passed · 3 min ago</p>
<p className="text-ink-1 text-sm font-medium">38 / 42 assertions passed</p>
<p className="text-ink-2 text-sm leading-relaxed">
4 cases failed on edge inputs. Review before promoting to production.
</p>
</TabsPanel>
<TabsPanel value="config" className="pt-4 space-y-2">
<p className="text-ink-3 font-mono text-2xs uppercase tracking-label">model</p>
<p className="text-ink-1 font-mono text-sm">claude-sonnet-4-6</p>
</TabsPanel>
</Tabs>
),
},
{
label: 'with disabled',
node: (
<Tabs defaultValue="active">
<TabsList>
<TabsTrigger value="active">Active</TabsTrigger>
<TabsTrigger value="disabled" disabled>Disabled</TabsTrigger>
<TabsTrigger value="other">Other</TabsTrigger>
</TabsList>
<TabsPanel value="active" className="pt-4">
<p className="text-ink-2 text-sm">This tab is active and its panel is visible.</p>
</TabsPanel>
<TabsPanel value="other" className="pt-4">
<p className="text-ink-2 text-sm">Other panel content.</p>
</TabsPanel>
</Tabs>
),
},
]}
/>
);
}
import { Tabs, TabsList, TabsTrigger, TabsPanel } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
import { HeroSpecimen } from '../_components/HeroSpecimen';
export default function TabsSnapshot() {
return (
<VariantGrid
title="Tabs"
hero={
<HeroSpecimen>
<Tabs defaultValue="results">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
</TabsList>
<TabsPanel value="overview" className="pt-3">
<p className="text-ink-2 text-xs">Overview panel.</p>
</TabsPanel>
<TabsPanel value="results" className="pt-3">
<p className="text-ink-1 text-sm font-medium">38 / 42 passed</p>
</TabsPanel>
</Tabs>
</HeroSpecimen>
}
variants={[
{
label: 'default',
node: (
<Tabs defaultValue="results">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="results">Results</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
</TabsList>
<TabsPanel value="overview" className="pt-4 space-y-2">
<p className="text-ink-1 text-sm font-medium">Response Quality</p>
<p className="text-ink-2 text-sm leading-relaxed">
Evaluates clarity, depth, and factual correctness of model output across 42 test cases.
</p>
</TabsPanel>
<TabsPanel value="results" className="pt-4 space-y-2">
<p className="text-ink-3 font-mono text-2xs uppercase tracking-label">passed · 3 min ago</p>
<p className="text-ink-1 text-sm font-medium">38 / 42 assertions passed</p>
<p className="text-ink-2 text-sm leading-relaxed">
4 cases failed on edge inputs. Review before promoting to production.
</p>
</TabsPanel>
<TabsPanel value="config" className="pt-4 space-y-2">
<p className="text-ink-3 font-mono text-2xs uppercase tracking-label">model</p>
<p className="text-ink-1 font-mono text-sm">claude-sonnet-4-6</p>
</TabsPanel>
</Tabs>
),
},
{
label: 'with disabled',
node: (
<Tabs defaultValue="active">
<TabsList>
<TabsTrigger value="active">Active</TabsTrigger>
<TabsTrigger value="disabled" disabled>Disabled</TabsTrigger>
<TabsTrigger value="other">Other</TabsTrigger>
</TabsList>
<TabsPanel value="active" className="pt-4">
<p className="text-ink-2 text-sm">This tab is active and its panel is visible.</p>
</TabsPanel>
<TabsPanel value="other" className="pt-4">
<p className="text-ink-2 text-sm">Other panel content.</p>
</TabsPanel>
</Tabs>
),
},
]}
/>
);
}
Props
Surface specific to <Tabs />.
| Prop | Type | Default | Description |
|---|---|---|---|
| onValueChange | ((value: string) => void) | — | — |
| orientation | "horizontal" | "vertical" | — | Layout axis. Drives arrow-key behaviour (left/right vs. up/down) and the
data-orientation attribute on the tablist.
@defaultValue 'horizontal' |
| dir | "ltr" | "rtl" | — | Reading direction. 'rtl' reverses horizontal arrow navigation. |
| activationMode | "manual" | "automatic" | — | Whether focusing a tab activates it automatically, or requires Enter/Space. @defaultValue 'automatic' |
| value | string | — | — |
| defaultValue | string | — | — |
Sub-components
Composition slots re-exported from the same module.
TabsList
No library-specific props. Pass through standard HTML attributes for the underlying element.
TabsTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | — |
| icon | ReactElement<unknown, string | JSXElementConstructor<any>> | — | Optional leading icon. Pass a ReactElement (e.g. a lucide-react icon). |