Usage
Tabs pairs triggers with linked content panels on a single route. It fits settings categories, incident summaries with peer sections, and dashboards where each tab reveals closely related content without an additional navigation hop. The mental model is "peer sections that share context"—if the content belongs on separate routes or users expect bookmarkable URLs per panel, reach for Tabs instead. Tabs is the pattern that stays on one URL; the triggers and panels are programmatically associated for assistive technologies so the label, state, and content are announced together.
Tabs. Tabs create linked content panels; a filter strip changes what a single panel renders.
Tabs. Users expect a link-based component to change the URL and be bookmarkable; tabs imply a single-route container.
Seven is already crowded. Above that, restructure the information architecture: a sidebar, a stepped flow, or nested pages.
Use Accordion when the mental model is "a tall list where I open a couple of sections", not "a row of peer panels I switch between."
| Axis | Values | When to pick it |
|---|---|---|
| Orientation | horizontal, vertical | Horizontal for short label lists atop panels; vertical when labels are long or sit beside a single wide panel |
| State | uncontrolled (defaultValue) or controlled (value + onValueChange) | Controlled when panel changes drive analytics, URL state, or cross-panel validation |
| Fill height | fill on panel Tabs (default false) | When the tab set should use the block size a flex parent assigns and the selected panel scrolls inside itself—no wrapper div for height or overflow |
| Tab list overflow | listOverflow on TabsList or link Tabs (TabsListOverflow.Visible or Scroll) | Scroll when the tab row can exceed its column; roving focus scrolls the active tab into view. Navbar and panel tab strips that collapse into a Menu keep their own overflow patterns |
Tabs wraps a TabsList of TabsTrigger buttons followed by one TabsContent panel per value. Labels sit in the trigger; optional counts use the counter affordance so operators can see magnitude without opening the tab. Keep labels short and parallel; verbose labels crowd the list and push counts off-screen.
For a full-height workspace (for example a call-taking panel), pass fill on panel Tabs and put the set in a flex parent with a definite block size. The root grows into that space and the selected TabsContent scrolls inside itself—do not wrap Tabs in a raw div only to assign height or panel scroll.
When many tabs share one row, pass listOverflow={TabsListOverflow.Scroll} on TabsList (or on link Tabs). The list scrolls on its layout axis, reserves padding so focus rings stay visible in the scrollport, and arrow-key roving focus scrolls the active tab into view.
counter prop; do not bake numbers into label text, where translations will break the rhythm.formatMessage. Even short English labels lengthen in German or French; test the longest expected translation inside the list width.defaultValue covers static settings views where no other UI depends on the active tab.value + onValueChange when syncing to URL params, analytics, or cross-panel validation. Drive the value from the source of truth; do not fork state across two parents.Tabs from @prepared911/ui/tabs implements the WAI-ARIA Tabs pattern: tablist, tab, and tabpanel roles with arrow-key navigation and focus management. The app still supplies the list's accessible name and the trigger labels. With listOverflow={TabsListOverflow.Scroll}, keyboard behavior is unchanged; focus moves among triggers as usual and the list scrolls to keep the focused tab visible. See Accessibility for the full keyboard map, announcement sequences, and labelling patterns.