Tabs
Switch between sections of related content within a single view. Built on base-ui Tabs — keyboard navigation, roving focus, and ARIA wiring come for free.
Compose four parts: Tabs (root, owns the active value), TabsList (the trigger strip), TabsTrigger (one per section), and TabsContent (the panel for a value).
Track variants follow the same chrome language as buttons: default has a border and track fill, outline is stroke-only, ghost is bare (hover/active on items). line is the underline strip for page nav. Use gap on Tabs for space between the list and panel, or on TabsList for space between triggers. Both accept spacing-scale numbers or raw CSS values.
Default
Bordered recessed track — the usual segmented control. Active trigger lifts onto a raised neutral chip with a hairline border.
Outline
variant="outline" keeps the outer stroke but drops the track fill — useful on surfaces where a filled rail is too heavy.
Ghost
variant="ghost" has no rail (no border, no track fill). Inactive items get a soft hover wash; active is the same raised chip. Good for in-dialog side nav and light filter strips.
Line (underline)
variant="line" drops the track and marks the active tab with a 2px underline. Right for page- or section-level navigation above content — e.g. Users / Permissions on a role detail page.
The underline is neutral (--foreground) by default. Add accent for a brand-colored underline — reserve it for when the tabs should draw attention: <TabsList variant="line" accent>.
With icons
Each TabsTrigger lays out its children in a row with a gap, and auto-sizes any leading svg to size-4. Drop a lucide icon in front of the label.
Sizes
size controls trigger height and text size: md (default) or sm for dense toolbars. Set it once on TabsList — triggers inherit it.
size="sm" — compact, for dense toolbars.size="md" — the default.Vertical orientation
Set orientation="vertical" on the root to stack the list beside its content. Paired with variant="ghost", this is the in-dialog settings-nav pattern (Profile / Preferences). The demo uses gap={6} on the root rather than a call-site gap class.
Controlled
Tabs is uncontrolled by default — give it defaultValue. To drive it yourself (e.g. to sync the active tab to the URL), pass value and onValueChange.
const [tab, setTab] = useState<"users" | "permissions">("users");
<Tabs value={tab} onValueChange={(v) => setTab(v as typeof tab)}>
<TabsList variant="line">
<TabsTrigger value="users">Users</TabsTrigger>
<TabsTrigger value="permissions">Permissions</TabsTrigger>
</TabsList>
{/* … */}
</Tabs>;Reference
Tabs
Root container. Extends base-ui Tabs.Root props.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | any | — | Initially active tab value (uncontrolled) |
value | any | — | Active tab value (controlled) |
onValueChange | (value) => void | — | Fires when the active tab changes |
orientation | "horizontal" | "vertical" | "horizontal" | Layout direction; also moves the active indicator |
gap | number | string | — | Gap between list and panel (number = spacing scale; string = CSS) |
className | string | — | Merged onto the root |
TabsList
The trigger strip. Extends base-ui Tabs.List props.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "outline" | "ghost" | "line" | "default" | Bordered track / stroke-only / bare / underline |
size | "md" | "sm" | "md" | Trigger height (md = h-9, sm = h-8) and text size; inherited by triggers |
gap | number | string | — | Flex gap between triggers (number = spacing scale; string = CSS) |
accent | boolean | false | line only — accent-colored underline instead of the neutral one |
fullWidth | boolean | false | Stretch the list and distribute triggers across its width |
TabsTrigger
One tab button. Extends base-ui Tabs.Tab props.
| Prop | Type | Default | Description |
|---|---|---|---|
value | any | — | Identifies which panel this opens |
TabsContent
A panel. Extends base-ui Tabs.Panel props.
| Prop | Type | Default | Description |
|---|---|---|---|
value | any | — | Matches the trigger that reveals it |