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, onValueChangeBase UI Tabs.RootTabsList size'default' | 'sm'default'default'TabsList activateOnFocusbooleandefaulttrueTabsTrigger value, disabledBase UI Tabs.TabTabsContent value, keepMountedBase UI Tabs.PanelTabsCountspan
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)