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>
)
}Pill (default)#
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#
Density#
Tabs ship at one tight size (a 14px label in a 32px-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.