Tabs
In-page panels with an edge rule and an active indicator.
Tabs puts several panels of content in one place and shows one at a time, with a list of
triggers to pick between them. The list is a 1px hairline on the edge that faces the panel,
and the active trigger carries a 2px bg-primary mark on that same edge. It has no well, so
for a sunken track of options that owns no panels, use
Toggle group.
Name and email for this workspace.
Usage
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
<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>Give the root a defaultValue, or a value and onValueChange, so one tab is active from
the start. Without one, no tab is active, no panel shows, and the mark is not drawn. Each
TabsContent takes the value of its trigger, and only the active panel is mounted.
The mark
The list is h-10 and draws its hairline in bg-border along the bottom edge. The active
trigger turns text-fg, and the mark sits on that hairline under it, 2px thick in
bg-primary. When the active tab changes, the mark stretches toward the new tab. Its
leading edge moves in --duration-fast and its trailing edge follows in --duration-base
with --ease-out, so it looks like it reaches and then catches up. The first time it
appears, and under reduced motion, it jumps instead. It re-measures when the list or a
trigger resizes, so a window resize or a trigger that changes width keeps it in place.
Vertical
orientation="vertical" puts the list beside the panel and rotates the hairline and the mark
to the inline-end edge, where the mark is 2px wide. Triggers fill the width of the list and
align to the start, and the arrow keys change to Up and Down.
Name and email for this workspace.
<Tabs defaultValue="account" orientation="vertical">
<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>Groups
TabsGroup gathers a run of triggers under a label inside a vertical list, like the parts of a
settings area that belong together. The label sits in the caption style and the group's
triggers are indented under it. They are still triggers of the same list, so the arrow keys move
through them in order with the rest, and the mark slides into the group like any other tab.
The people who sign, and what each signature says.
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsGroup label="Campaign details">
<TabsTrigger value="business">Your business</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
</TabsGroup>
</TabsList>Icons and disabled tabs
iconBefore on a trigger is the same slot as on Button and Chip. It takes an icon component
and draws it 16px in text-primary, and the icon is hidden from assistive tech, so a trigger
with only an icon needs an aria-label.
Name and email for this workspace.
disabled keeps a trigger in the list at reduced opacity. It cannot be clicked and the arrow
keys skip it.
First panel.
Props
Tabs takes every prop of the Radix Tabs parts, such as value, defaultValue,
onValueChange, orientation, dir, and activationMode on Tabs, loop on TabsList,
and value, disabled, and forceMount on TabsTrigger and TabsContent, plus these.
| Prop | Type | Default | What it does |
|---|---|---|---|
iconBefore | ComponentType<{ className?: string }> | none | On TabsTrigger. Draws a 16px icon in text-primary before the label. |
label | string | required | On TabsGroup. The group's name, shown above its triggers and read as each one's description. |
Accessibility
TabsListis atablistwitharia-orientationset fromorientation. Each trigger is atabwitharia-selectedandaria-controlspointing at its panel, and each panel is atabpanelwitharia-labelledbypointing back at its trigger.- The list is one tab stop, and Tab lands on the active tab. The arrow keys for the orientation, Left and Right or Up and Down, move between triggers and wrap at the ends, and Home and End jump to the first and last. Disabled triggers are skipped.
- Activation is automatic, so moving focus to a tab with the arrow keys also shows its
panel. Set
activationMode="manual"onTabsand the arrow keys only move focus, and Space or Enter shows the panel. - A panel is itself a tab stop, so Tab from the list goes into the panel, and it shows a 2px
focus ring on
:focus-visible. A trigger shows the same ring. - A trigger inside
TabsGroupis described by the group's label througharia-describedby, so a screen reader says "Team, tab, Campaign details". The label itself is plain text, not a heading, since the list stays one set of tabs. - Triggers are at least 40px tall and the list is 40px tall in both orientations. There is no larger target on a touch screen.
- Under reduced motion the mark jumps without stretching and the trigger color changes without a transition.
- In forced colors the hairline and the mark are background fills, so they do not show. The
active tab is still reported through
aria-selected, but nothing on screen marks it apart from its text color, which the system may replace.
When to use something else
- Toggle group when the options change the view you are already in and own no panels. Tabs gives each option a panel and the tab roles, and Toggle group only reports a value in a sunken well.
- Tab switcher is the same well built from a
tabsarray, for two or three views or modes. It is not a tablist, so use Tabs when there are panels. - Sidebar when the places are the pages of the app, with groups, counts, and routes, rather than panels of one page.
- Stepper when the steps are ordered and the reader moves through them in sequence.
- Pagination when the panels are pages of one list.
- Accordion when the sections are stacked and more than one may stay open.
Install
shadcn add @gesso/tabs