Components / disclosure

Tabs

Radix is permitted here — Tabs is on the closed list of six.

Rendered from the package · open on its own

Summary

Alternative views of one thing, one visible at a time.

APG patternTabs
Built onRadix
Statusstable
Since0.1.0
<Tabs label="Project view" activation="automatic" tabs={[{ id: 'edit', label: 'Edit', content: … }]} />

Anatomy

A tablist of tabs over one visible panel.

PartElementRequired
label<span>yes
list<radixtabs.list>yes
panel<radixtabs.content>yes
tab<radixtabs.trigger>yes

Properties

PropertyValuesDefaultWhat it does
tabsTabItem[]requiredThe tabs and their panels, in display order. Each needs a stable id.
valuestringnoneThe selected tab. Passing it makes the set controlled — which is what you want if the selection belongs in the URL.
defaultValuestringnoneWhich tab starts selected. Defaults to the first.
onValueChange(value: string) => voidnoneCalled with the newly selected tab id.
activationautomatic · manualautomaticautomatic selects on focus — right when panels are cheap. manual waits for Enter or Space, so arrowing past tabs does not fire a request each time.
orientationhorizontal · verticalhorizontalWhich way the tablist runs. vertical stacks the tabs into a column beside the panel, moves the indicator to each tab's inline end, and Up and Down move between tabs instead of Left and Right.
labelstringnoneNames the tab list. Required when a page has more than one set.
labelledBystringnoneId of a visible element that names the tab list. Wins over label.
lazybooleanfalseMount each panel only while it is selected. Cheaper for expensive panels, but the unselected tabs' aria-controls then point at nothing.

States

StateExpressed as
Disabled:disabled
Focused:focus-visible
Hover:hover
Selectedaria-selected
Open or closeddata-state

Behavior

automatic selects a tab as soon as focus lands on it — correct when panels are already loaded. Switch to manual when opening a panel costs a request, so arrowing across four tabs does not fire four loads.

Arrow keys move between tabs; the set is one tab stop.

orientation="vertical" stacks the list into a column beside the panel. The indicator moves to each tab's inline end, sitting on the same shared hairline the underline sits on horizontally, and movement passes to Up and Down. A CSS-only consumer writes data-orientation="vertical" on the root and nothing deeper — the selectors reach no further.

Accessibility

The tablist is named, so a page with two sets of tabs has two distinguishable ones.

Radix parks the tab stop on the tablist root rather than on an item, which satisfies the one-tab-stop requirement differently from the APG's own example. The port asserts the invariant rather than the number.

Keyboard

KeyResult
Arrow keysMove between tabs — Left and Right horizontally, Up and Down when orientation="vertical". With activation="automatic" the panel changes as focus lands
Enter / SpaceSelects the focused tab, which is what activation="manual" waits for
Home / EndJump to the first or last tab
TabLeaves the tablist and enters the panel

Roles, states and properties

AttributeSet by
aria-labelset at render
aria-labelledbyset at render

Focus

The tablist is one stop. Radix parks it on the tablist root rather than on a tab, which satisfies the one-stop requirement differently from the APG's own example — the port asserts the invariant rather than the number.

Screen reader

Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.

Contrast and target size

npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.

It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.

Tested

  • test/apg/partials-sweep.test.jsx

Known gaps

_No upstream requirement is recorded as not applicable._

Appearance

TokenUsed for
--se-accentborder-block-end-color, border-inline-end-color
--se-borderborder-block-end, border-inline-end
--se-border-widthborder-block-end, border-inline-end, margin-block-end, margin-inline-end
--se-control-hmin-block-size
--se-duration-instanttransition
--se-ease-standardtransition
--se-focus-coloroutline
--se-focus-offsetoutline-offset
--se-focus-widthborder-block-end, border-inline-end, outline
--se-icon-smblock-size, inline-size
--se-radius-smborder-radius
--se-space-1gap
--se-space-2gap, padding-block-end
--se-space-3padding-inline-end
--se-space-4gap
--se-target-minmin-block-size
--se-textcolor
--se-text-disabledcolor
--se-text-mutedcolor

Motion

The active tab's indicator crosses --se-duration-fast. Panels do not animate: a panel that slides in delays the content the reader just asked for.

Content

Tab labels are nouns, short, and parallel — Edit, Preview, Settings. One long label among three short ones makes the set look broken.

Usage

Tabs are not navigation. If the panels are really pages, or the reader should be able to link to one or use the back button, use links and routes. Tabs hide their content from search and from anyone you send the URL to.

Use them for alternatives, not for sequence. A wizard needs a visible sense of progress, which tabs deliberately do not give.

Notes

Radix is permitted here — Tabs is on the closed list of six.