Tabs
Switch between related panels. Built on Radix Tabs for behavior and a11y, styled with one tv slots recipe. The active state is a single indicator measured in JS and slid with transform.
Installation#
Usage#
import {
Tabs, TabsList, TabsTrigger, TabsContent,
} from "@/components/ui/tabs"
export function Example() {
return (
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">Account panel</TabsContent>
<TabsContent value="password">Password panel</TabsContent>
</Tabs>
)
}Anatomy#
import {
Tabs,
TabsList,
TabsTrigger,
TabsTriggerIcon,
TabsPanels,
TabsContent,
} from "@/components/ui/tabs"
<Tabs>
<TabsList>
<TabsTrigger>
<TabsTriggerIcon icon={…} />
</TabsTrigger>
</TabsList>
<TabsPanels>
<TabsContent />
</TabsPanels>
</Tabs>Segmented (default)#
The iOS segmented control: a padded rounded-rect track with equal-width segments, and a hairline between resting segments that fades out beside the selected one.
Pill#
A pill trough with the thumb sliding inside it, sized to each label rather than shared equally. It is also the rail a vertical Tabs stands as when no variant is named.
Folder#
Line#
Chip#
A row of separate outlined chips for a showcase switcher, where each tab is a destination with its own icon rather than a view of one panel. The active chip settles onto a muted fill, so there is no sliding indicator. Chips take a 16px label with a 20px muted glyph, and paint an opaque fill (the ground they sit on) so the rail can be pinned with sticky over the artwork it switches. To fold the rail onto two rows on a phone, pass flex-wrap to TabsList.
Icons#
Selected icon#
TabsTriggerIcon lets the glyph mark the selection: the tab's own icon at rest, a filled check once it is the chosen one. Both stay mounted and cross-fade (opacity, scale and blur) on the trigger's state, so a quick switch back reverses mid-way instead of restarting. The check is the label's ink, not brand. Pass activeIcon for another mark. It suits a filter rail, where the selected chip says what the grid below is showing.
Density#
Tabs ship at one tight size (a 14px label in a 28px-tall trigger). density tightens the pill chrome for application UI: a snugger container and tighter concentric radii. Set it per tabs or for a whole subtree with DensityProvider - see Density.
Overflow#
When the tabs are wider than their container, the list scrolls horizontally in a single row instead of pushing the layout wide, so tabs never break the page on narrow viewports. Scrollbars are hidden across Koala, so the leading edge softly fades toward the tabs you can still scroll to. It is inert whenever the tabs already fit.
Vertical#
Pass orientation="vertical" and the rail stands beside its panel instead of above it: the reading-pane layout, for a docs sidebar or a FAQ whose answers should not move the page. It is forwarded straight to Radix, so the arrow keys become up and down and the ARIA follows; Radix stamps data-orientation on the root, the list and every trigger, so the whole layout is one set of variants with no second prop to keep in sync. A vertical rail is a nav rather than a segmented control, so the trough stands down and the indicator becomes the selected row's own fill; rows grow to a 40px reading row and their labels wrap rather than forcing the rail wider. The sliding indicator needs nothing: it already tracks the active trigger's box, whichever way the list runs.
Panel height#
Radix unmounts the panel you leave and mounts the one you arrive at, so panels of different heights make the block below the tabs snap from one to the other while the new panel is still fading up: two gestures fighting over the same moment. Wrap them in TabsPanels and the block eases to the new panel's height instead, with the fade riding along as one movement. It is the same measured box the Dock morphs with (useMorphBox).
It is opt-in because it clips, which is exactly what makes the reveal read as one object. A panel that has to paint outside its own box (a sticky header, a popover that is not portalled) should stay unwrapped, and so should one that can grow without a ceiling: past a certain height the ease stops reading as a gesture and turns back into a jump.
API reference#
Tabs
The root, Radix Tabs.Root; shares the variant and density with every part. Forwards all Radix Root props; every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string | - | The tab selected on mount, when uncontrolled. |
| value | string | - | The selected tab, when controlled. |
| onValueChange | (value: string) => void | - | Called when the selected tab changes. |
| variant | "segmented" | "pill" | "folder" | "line" | "chip" | "segmented" (vertical: "pill") | iOS-style padded segmented control with equal-width segments, pill trough, filled tab over a sliding bar, bare sliding underline, or separate outlined chips with no indicator. |
| density | "comfortable" | "compact" | "compact" | Tightens the pill trough and the concentric radii; the label size stays. Falls back to the nearest DensityProvider. |
| orientation | "horizontal" | "vertical" | "horizontal" | vertical stands the rail beside the panel, with up and down arrow keys. |
| activationMode | "automatic" | "manual" | "automatic" | Whether a tab is selected on focus or only on click and Enter. |
| dir | "ltr" | "rtl" | - | Reading direction for arrow-key navigation. |
| Data attribute | Description |
|---|---|
| data-orientation | "horizontal" or "vertical". |
TabsList
The row of triggers, Radix Tabs.List, with the sliding indicator inside. Scrolls sideways with an edge fade when the tabs overflow; pass flex-wrap to wrap instead. Forwards all Radix List props.
| Prop | Type | Default | Description |
|---|---|---|---|
| loop | boolean | true | Whether arrow keys wrap from the last tab to the first. |
| Data attribute | Description |
|---|---|
| data-orientation | "horizontal" or "vertical". |
TabsTrigger
One tab, Radix Tabs.Trigger rendering a <button>. A nested svg is sized to 16px (20px in chip). Forwards all Radix Trigger props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | Pairs the trigger with the TabsContent of the same value. |
| disabled | boolean | false | Skips the tab in keyboard navigation and blocks selection. |
| asChild | boolean | false | Merge onto your own element instead of rendering a <button>. |
| Data attribute | Description |
|---|---|
| data-state | "active" or "inactive". |
| data-orientation | "horizontal" or "vertical". |
| data-disabled | Present when disabled. |
TabsTriggerIcon
A glyph that marks the selection: the tab's icon at rest, cross-fading to a check once selected. Decorative (aria-hidden). Forwards all native <span> props except children.
| Prop | Type | Default | Description |
|---|---|---|---|
| icon* | React.ReactNode | - | The tab's own glyph, shown while it is not selected. |
| activeIcon | React.ReactNode | <CheckCircle weight="fill" /> | The glyph once the tab is selected. |
TabsPanels
Optional wrapper that eases its height to the incoming panel instead of snapping. It clips, so leave out panels that paint outside their box or grow without a ceiling. Forwards all native <div> props.
| Data attribute | Description |
|---|---|
| data-morph-ready | Present once the first height is measured; arms the transition. |
TabsContent
One panel, Radix Tabs.Content; fades up when it becomes active. Forwards all Radix Content props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | Pairs the panel with the TabsTrigger of the same value. |
| forceMount | true | - | Keep the panel mounted while inactive, e.g. to animate it yourself. |
| Data attribute | Description |
|---|---|
| data-state | "active" or "inactive". |
| data-orientation | "horizontal" or "vertical". |
tabsVariants
The tv recipe behind every part, for styling a rail of your own to match.