Tabs
Tabs switch between panels of content. The root is Headless UI's TabGroup
(roving focus, arrow keys, role="tablist" / tab / tabpanel wiring); the
compound parts read variant, size, orientation and fullWidth from the
root so you set them once.
import Tabs from '@zuilib/components/tabs'
Variants
underline draws a --primary bar under the selected tab. pills is a
segmented control on a --muted track. enclosed frames the selected tab
like a folder tab joined to the panel edge.
Sizes and icons
sm, md and lg read --control-height-*, --control-padding-x-* and
the matching --text-* step. icon renders before the label, sized to the
tab; the slot is aria-hidden, so an icon-only tab needs an aria-label.
Vertical and full width
vertical stacks the list beside the panels and makes the arrow keys move
up and down. fullWidth stretches the tabs to share the list (flex-1).
Controlled and manual
selectedIndex + onChange controls the selection; defaultIndex is the
uncontrolled start. manual makes the arrow keys only move focus, so a tab
is selected with Enter or Space (by default focusing a tab selects it).
Props
TabsProps extends the <div> attributes (minus onChange) with:
| Prop | Type | Default | Description |
|---|---|---|---|
selectedIndex | number | — | Controlled selected tab |
defaultIndex | number | 0 | Uncontrolled initial tab |
onChange | (index: number) => void | — | |
variant | 'underline' | 'pills' | 'enclosed' | 'underline' | |
size | 'sm' | 'md' | 'lg' | 'md' | --control-height-* / --control-padding-x-* |
vertical | boolean | false | List beside the panels; arrows move up / down |
fullWidth | boolean | false | Tabs share the list (flex-1) |
manual | boolean | false | Arrows only move focus; Enter / Space selects |
className | string | — | Merged last onto the root |
Tabs.Tab
TabsTabProps extends the <button> attributes minus role, tabIndex,
aria-selected and aria-controls (owned by Headless UI) with:
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Before the children, sized to size; aria-hidden |
disabled | boolean | false | |
autoFocus | boolean | false | |
children | ReactNode | (state) => ReactNode | — | Render prop receives {selected, hover, focus, active, disabled} |
className | string | — | Merged last onto the tab |
Tabs.Panel
TabsPanelProps extends the <div> attributes minus role and
aria-labelledby with:
| Prop | Type | Default | Description |
|---|---|---|---|
static | boolean | false | Always render the panel, ignoring selection |
unmount | boolean | true | false keeps unselected panels mounted but hidden |
tabIndex | number | 0 | |
className | string | — | Merged last onto the panel |
Tabs.List (minus role / aria-orientation) and Tabs.Panels take the
plain <div> attributes plus className. All parts are also named exports
(TabsList, TabsTab, TabsPanels, TabsPanel).
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="tabs"] | div | Root; data-variant, data-size, data-orientation |
[data-slot="tabs-list"] | div | role="tablist"; scrolls along its axis when the tabs outgrow it |
[data-slot="tabs-tab"] | button | role="tab"; Headless UI data-selected / data-hover / data-focus / data-disabled |
[data-slot="tabs-tab-icon"] | span | Wraps icon; aria-hidden |
[data-slot="tabs-panels"] | div | — |
[data-slot="tabs-panel"] | div | role="tabpanel"; data-selected / data-focus |
/* Consumer CSS: thicker underline */
[data-slot="tabs"][data-variant="underline"] [data-slot="tabs-tab"] {
border-bottom-width: 3px;
}
Accessibility
- Headless UI sets
role="tablist"witharia-orientation,role="tab"witharia-selected/aria-controls, androle="tabpanel"witharia-labelledby. Thearia-controls/aria-labelledbyids resolve from refs, so they appear after hydration rather than in server markup. - Roving tabindex: only the selected tab is in the tab order. Left / Right (Up / Down when
vertical) move between tabs and wrap; Home / End jump to the first / last. Focus selects unlessmanual, where Enter / Space select. - The keyboard focus ring is an inset
--ringring ondata-[focus], on tabs and on the focused panel. - Disabled tabs are skipped by the arrow keys and get
data-disabled(50% opacity, no pointer events). - The
iconslot isaria-hidden; an icon-only tab must carryaria-label.
Related
- Accordion and Disclosure: stacked show / hide instead of switching panels.
- StepIndicator: ordered progress through views.
- Theming: tab boxes follow the control tokens.