Browse the docs
Tabs
Tabbed panels following the WAI-ARIA tabs pattern: arrow keys (and Home/End) move between tabs, selection follows focus, only the selected tab is in the tab order, and each panel is associated with its tab. The behavior lives in the tabs primitive; this component styles it and takes the panels as data.
This component needs the tabs primitive — two ways to get it:
- Copy: fetch
/r/primitives/tabs.txt— one file with the shared core embedded — and save it asprimitives/tabs.js. - Install:
npm install @presetui/ui— the primitives are bundled, no second package — then use the Source · npm tab below, whose import already points at@presetui/ui/primitives/tabs.
Props
| Prop | Values | Default | Description |
|---|---|---|---|
items | array | required | One object per tab: { value, label, content }. |
defaultValue | string | first item | Which tab starts selected. |
variant | "line", "pill", "none" | "line" | Underlined tabs, a segmented control, or undecorated — style "none" yourself via data-[state=active]. |
className | string | "" | Appended to the outer wrapper. |
Common patterns
<Tabs
items={[
{ value: "overview", label: "Overview", content: <Overview /> },
{ value: "members", label: "Members", content: <Members /> },
]}
/>
// Start on a specific tab
<Tabs defaultValue="members" items={items} />Notes
Panels are data, not composition. A Tabs/TabsList/TabsTrigger/TabsContent
set would need React context to pass the selected value around, and context
inside a copy-paste file is more machinery than this earns. Passing an array
keeps the whole component in one file you can read at a glance.
The trade-off: content for every tab is constructed on render, even the hidden ones. If a panel is expensive, pass a component that fetches its own data rather than the data itself.
Only the selected panel is mounted, so state inside an inactive panel is lost when you switch away. Lift it out if you need it to survive.