Tabs
Tabbed panels following the WAI-ARIA tabs pattern: arrow keys move between tabs, only the selected tab is in the tab order, and each panel is associated with its tab.
This is the first component that holds state, so it imports useState from
React and carries a "use client" directive.
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" | "line" | Underlined tabs, or a segmented control. |
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.