Tabs
A navigation control on Ark UI's Tabs machine — a row (or column) of triggers, each revealing its own content panel, with a pill that slides to the active trigger. Visually SegmentGroup's twin.
Tabs is a navigation control: a row (or column) of triggers, each revealing its own content panel, with a pill that slides to the active trigger. Visually it's SegmentGroup's twin — the same filled / light / outline / subtle / default variant × color system and the same sliding-pill mechanics — but built for real content panels instead of a single-choice value, with no Field integration (Tabs is navigation, not a form control).
import { Tabs } from '@42-central/ui-react/tabs';
<Tabs
defaultValue="overview"
data={[
{ value: 'overview', label: 'Overview', content: <Overview /> },
{ value: 'activity', label: 'Activity', content: <Activity /> },
]}
/>Playground
Component
Dir
Presets
Props
Code
<Component
orientation="horizontal"
variant="default"
size="md"
/>Items
data is a flat array of { value, label, content, disabled? } — each tab needs its own content, so (unlike SegmentGroup) there's no bare-string shorthand.
<Tabs
defaultValue="overview"
data={[
{ value: 'overview', label: 'Overview', content: <Overview /> },
{ value: 'activity', label: 'Activity', content: <Activity /> },
{ value: 'settings', label: 'Settings', content: <Settings />, disabled: true },
]}
/>Each item also accepts an optional testId, defaulting to `tab-${value}` — the matching
content panel gets `${testId}-panel`. See the Testing guide. The compound
Tabs.Trigger / Tabs.Content / Tabs.List / Tabs.Root / Tabs.Indicator parts each take
their own testId prop directly, for the same reason.
Variants
Five treatments, mirroring Button and SegmentGroup. The variant restyles the tray, every trigger, and the sliding pill off the same --c-* color slots. filled is a solid pill, light a soft tint, outline a ringed pill over the tray, subtle is chromeless. default is the same solid pill as filled on a lighter tray — with no color set it resolves to black (white in dark theme), and it tints like every other variant when you give it one.
Code
<Tabs variant="filled" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="light" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="outline" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="subtle" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="default" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />Sizes
Five sizes — xs through xl — matching the rest of the family. Sizing lives on the triggers; the tray's own gap scales alongside.
Code
<Tabs size="xs" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="sm" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="md" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="lg" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs size="xl" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />Colors
The resting triggers are palette-independent; only the sliding pill (and the active label's text) adopt color. Set it once and every kit palette tints for free — the active label follows the pill onto --c-on-solid, so it stays legible whichever palette you pick. color is scoped to the tab strip: it does not tint whatever your panels render, so a Button inside a panel keeps its own palette.
Code
<Tabs variant="filled" color="brand" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="green" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="blue" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="filled" color="purple" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />
<Tabs variant="default" color="teal" defaultValue="a" data={[{ value: 'a', label: 'Overview', content: 'A' }, { value: 'b', label: 'Activity', content: 'B' }]} />Orientation
Set orientation="vertical" to stack the triggers in a column instead of a row.
Disabled tab
A single tab can be disabled via { value, label, content, disabled: true }. It's skipped by keyboard navigation and can't be clicked.
Compound API
Drop to the composable parts for anything the flat data array can't express — custom trigger content, interleaved non-tab elements, or driving the value from your own state. variant, size and color all live on Tabs.List and apply to the tray, every trigger, and the indicator. color is a data-color attribute on the list specifically — putting it on Tabs.Root would also cover every Tabs.Content, leaking the palette into your panel content.
'use client';
import { Tabs } from '@42-central/ui-react/tabs';
<Tabs.Root defaultValue="overview">
<Tabs.List variant="filled" color="brand">
<Tabs.Indicator />
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview"><Overview /></Tabs.Content>
<Tabs.Content value="activity"><Activity /></Tabs.Content>
</Tabs.Root>Controlled
Drive the value from state. onChange hands you the selected value directly (the data-driven form); the compound Tabs.Root takes Ark's own value / onValueChange.
'use client';
import { useState } from 'react';
import { Tabs } from '@42-central/ui-react/tabs';
export function ProjectTabs() {
const [tab, setTab] = useState('overview');
return (
<Tabs
value={tab}
onChange={setTab}
data={[
{ value: 'overview', label: 'Overview', content: <Overview /> },
{ value: 'activity', label: 'Activity', content: <Activity /> },
]}
/>
);
}API
Prop
Type
Accessibility
- Built on Ark UI's Tabs machine:
role="tablist"/"tab"/"tabpanel", witharia-selectedand rovingtabIndexon the triggers andaria-controls/aria-labelledbylinking each trigger to its panel. ←/→(or↑/↓whenorientation="vertical") move focus between triggers;activationMode="automatic"(the default) activates the focused tab immediately,"manual"waits forEnter/ a click.Tabenters and leaves the tablist as a single stop.- A
disabledtrigger is skipped by arrow-key navigation and unclickable. - RTL is honored via the document
dir.