Tabs
Switches between views within a panel, where only one view is needed at a time.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/tabs.jsonUsage
import { Tabs, TabsList, TabsTab, TabsPanel } from '@/components/ui/tabs'<Tabs defaultValue="images">
<TabsList>
<TabsTab value="images">Images</TabsTab>
<TabsTab value="report">Report</TabsTab>
</TabsList>
<TabsPanel value="images">…</TabsPanel>
<TabsPanel value="report">…</TabsPanel>
</Tabs>Tabs are for alternatives — views of one thing, only one of which is wanted at a time. They are the wrong choice when the reader has to compare two panels, when the panels are steps of a sequence, or when there are enough of them to wrap onto a second row. Content that needs to be scanned together belongs in an Accordion or simply stacked; a sequence wants a stepper; a long list of destinations wants navigation, not tabs.
Nothing hidden behind a tab is discoverable by find-in-page, so never put the one number a reader came for on the second tab.
Composition
<Tabs> {/* owns the value and the orientation */}
<TabsList> {/* the tab strip; carries the variant */}
<TabsTab> {/* one tab, keyed by value */}
</TabsList>
<TabsPanel> {/* the content for a value */}
</Tabs>Tabs is a flex column that becomes a flex row under orientation="vertical", which is what
puts a side list beside its panel with no layout of your own. Panel and tab are paired by
value; Base UI wires aria-controls and aria-labelledby between them.
Underline
The default, and the one to use for tabs at the top of a page or a card — the same
TabComponentTop the design system shipped. The marker is inset 8px from each edge of the tab
rather than spanning it, and it is a pseudo-element rather than a border so it can be.
An icon goes before the label rather than instead of it. A tab strip is the one place an icon-only control rarely pays: the labels are what tell the reader what is behind each tab before they commit to clicking it.
Pill
A filled track for switching how one thing is displayed rather than moving between sections of
it — plane, layout, units. The list is w-fit, so it stays the width of its tabs instead of
stretching across the panel and reading as a toolbar.
Two or three pills that toggle a setting rather than a view are a Toggle Group, not tabs — tabs must have panels.
Side
- Patient ID
- PT-88213
- Sex
- F
- Age at study
- 64
Vertical placement, for a long list of sections beside their content: study metadata, settings,
a DICOM header browser. Pass orientation="vertical" on the root as well as variant="side" on
the list — the variant is the look, the orientation is what tells Base UI that Up and Down are
the arrow keys that move between tabs.
Counts in the label
A worklist tab is more useful with its depth on it. Keep the count inside the TabsTab so it is
part of the tab's accessible name — a badge rendered as a sibling reads as an unrelated "23", and
one placed before the label makes the tab announce itself as a number.
Counts that change while the reader is looking at them are a live region problem, not a tabs
problem. Do not add aria-live to a tab.
Controlled
Pass value and onValueChange when something outside the strip has to move the selection — a
"next section" button, a deep link, or a validation failure that must reveal the tab holding the
offending field. TabsTab.Value is any, so cast or narrow the value if your state is typed as
a string.
Keyboard
Arrow keys move between tabs and select as they go, Home and End jump to the ends, and Tab from
the strip lands on the panel rather than the next tab — one tab stop for the whole set. Under
orientation="vertical" the arrows become Up and Down. All of it arrives with the primitive.
Base UI activates on focus, which is right for panels that are already rendered and wrong for panels that fetch. If a panel is expensive, render its shell immediately and load into it, rather than making arrow-key navigation fire four requests.
API Reference
Everything Base UI's Tabs accepts. What we add on
TabsList:
Prop
Type
On Tabs:
Prop
Type
TabsTab takes value and disabled; TabsPanel takes value and keepMounted.
| Part | Selected state in the DOM |
|---|---|
TabsTab | data-active and aria-selected="true" — there is no data-selected |
TabsPanel | mounted, and hidden when keepMounted is set |
The stylesheet targets aria-selected. Reaching for data-selected by analogy with Select fails
silently here: the attribute does not exist on a tab, the rule matches nothing, and the strip
renders looking as though no tab is chosen.