Tabs

Switches between panels of one view.

Sizes

  • defaultdefault

  • sm

States

  • rest

  • hover

  • held

  • focus-visible

  • selected

  • disabled

Usage

When to use it
Use
One page or card shows two to seven panels of the same object, read one at a time: runs, traces, settings.
Use
A page, a card or a pane splits into sections with their own content.
Not for
The choice changes how one set of data shows, such as a log level or a chart range. Use Segmented control.
Not for
The person moves through the panels in order. Use Steps.
Not for
The places are the app's main areas, each with its own URL; link them from the app's navigation.

Rules

7 rules
Must
Give every TabsList an aria-label naming the object: "Agent", "Run". Because a screen reader announces an unnamed "tab list", so two tab sets on one page sound the same.
Must
Pass value or defaultValue on Tabs. Because with neither, no tab is selected on load, so no panel shows and the paper pill has nothing to sit under.
Never
Put tabs on a grey track or darken the selected pill; a track is the segmented control. Because tabs on a track look exactly like a segmented control, which changes a view of one data set, so the person cannot tell whether a press swaps the panel or filters it.
Must
Put a number after the label in TabsCount, never in the label text. Because TabsCount sets tabular figures at the label's weight, so 9 to 10 does not jump, and a bracketed number reads as part of the name.
Must
Pass keepMounted to a TabsContent that holds typed or scrolled state. Because Base UI unmounts a hidden panel, so switching away and back drops the typed text and the scroll position.
Should
Keep two to seven tabs, all about the same object, each a noun of one or two words. Because a tab bar reads as one object seen several ways, so Billing beside Runs hides a page people look for in the app's navigation.
Never
Add an icon to some tabs and not others. Because a tab with an icon or a different mark reads as selected or special, so the person cannot tell which panel shows.

State order

6 states, highest first
1
disabled
2
selected
3
held
4
focus-visible
5
hover
6
rest

Accessibility

5 checks
The list is a tablist named by aria-label; each trigger is a tab with aria-selected and aria-controls, each panel a tabpanel.
The selected tab reads by more than colour: it sits on the paper pill with a hairline, its label text-1 at 11.9:1 against text-2 ones.
Unselected labels are text-2, 5.5:1 on paper; disabled ones drop to the disabled grey and arrow keys skip them.
Focus is the 2px ink ring outside the pill, never clipped by the scroller.
Forced colors: the selected pill takes Highlight and its label HighlightText.

Keyboard

3 keys
Arrow Left, Arrow Right
Move to the next tab and select it, mirrored in right to left.
Home, End
Move to the first or last tab.
Tab
Enters on the selected tab, then moves into the panel.

Edge cases

8 edge cases
Tabs wider than the container
The list never wraps; it scrolls sideways with no bar and keeps the selected tab in view.
One tab
Render the panel alone: a tab list of one switches nothing.
Count changes live
Tabular figures keep each digit one width, so the tabs to its right barely move.
Panel loading or failing
The tab switches at once; the panel holds its Skeleton or Empty state.
Zoom 200%
Tabs grow with the text past 32px and the list scrolls.
Dark theme
The selected pill is the raised dark paper with a light hairline, the lightest thing in the row.
Reduced motion
The paper pill jumps to the selected tab instead of sliding.
Vertical
Not supported: a column of places is the app's navigation, plain links.

API

6 props
Tabs value, defaultValue, onValueChange
Base UI Tabs.Root
TabsList size
'default' | 'sm' default 'default'
TabsList activateOnFocus
boolean default true
TabsTrigger value, disabled
Base UI Tabs.Tab
TabsContent value, keepMounted
Base UI Tabs.Panel
TabsCount
span

Tokens

14 tokens
--paper
#ffffff
--line-faint
rgb(5 5 5 / 0.06)
--text-1
rgb(5 5 5 / 0.80)
--text-2
rgb(5 5 5 / 0.60)
--text-disabled
rgb(5 5 5 / 0.32)
--control
32px
--control-sm
26px
--radius-pill
999px
--radius-tag
6px
--type-label
500 13px/1.35 var(--font-sans)
--type-label-sm
500 12px/1.35 var(--font-sans)
--focus
rgb(5 5 5 / 0.80)
--dur-slide
200ms
--ease-out
cubic-bezier(0.2, 0.8, 0.2, 1)

Install

For your coding agent

GitHub

Please star me pookie

141

A new design system every week

Leave your email before you copy the prompt.