gray
radius
Navigation

Tabs

One view at a time from a small set. Radix supplies the roving tabindex and the aria-controls wiring, the parts that make a tab strip a tab strip rather than a row of buttons.

Basic

Focus a tab, then use the arrow keys. The strip is one tab stop; Tab again reaches the panel.

Color, spacing, radius, type, shadow and motion. Four consumption formats generated from one source.

<Tabs.Root defaultValue="tokens">
<Tabs.List>
<Tabs.Trigger value="tokens">Tokens</Tabs.Trigger>
<Tabs.Trigger value="base">Base</Tabs.Trigger>
<Tabs.Trigger value="components">Components</Tabs.Trigger>
<Tabs.Trigger value="archived" disabled>Archived</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="tokens">
<Text size={5} color="assistive">
Color, spacing, radius, type, shadow and motion. Four consumption
formats generated from one source.
</Text>
</Tabs.Panel>
<Tabs.Panel value="base">
<Text size={5} color="assistive">
Eighteen headless primitives. Radix where WAI-ARIA already specifies
the contract.
</Text>
</Tabs.Panel>
<Tabs.Panel value="components">
<Text size={5} color="assistive">
Twenty-three styled components, replaceable by design.
</Text>
</Tabs.Panel>
</Tabs.Root>

Size

Two, set on the list so every tab in a strip matches. 15 labels a section, 17 labels a page.

{(['m', 'l'] as const).map(size => (
<Tabs.Root key={size} defaultValue="tokens">
<Tabs.List size={size}>
<Tabs.Trigger value="tokens">Tokens</Tabs.Trigger>
<Tabs.Trigger value="base">Base</Tabs.Trigger>
<Tabs.Trigger value="components">Components</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
))}

Fill

Divides the strip evenly instead of sizing each tab to its label. For a strip that is the width of the thing it belongs to.

<Tabs.Root defaultValue="tokens">
<Tabs.List fill>
<Tabs.Trigger value="tokens">Tokens</Tabs.Trigger>
<Tabs.Trigger value="base">Base</Tabs.Trigger>
<Tabs.Trigger value="components">Components</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
The whole strip is one tab stop. Tab into it, then arrow between tabs and Tab again to reach the panel. That is what a screen reader user expects, and the part most hand-rolled tab strips miss.

Parts

Import it as a namespace, and the parts work on either side of the server boundary.

import * as Tabs from '@minuk-hwang-design-system/components-react/tabs';
PartWhat it is
Tabs.RootOwns the selected value.
Tabs.ListThe strip, with the rail under it.
Tabs.TriggerOne tab. Carries the indicator.
Tabs.PanelIts content. Named for what it is; Radix calls this Content, which reads oddly beside Dialog.Content.

Props

PropTypeDefaultNotes
Root.valuestringControlled selection.
Root.defaultValuestringUncontrolled initial tab.
Root.onValueChange(value: string) => voidFires with the chosen tab's value.
List.fillbooleanfalseDivides the strip evenly between the tabs.
List.size'm' | 'l''m'15 or 17. On the list, so every tab in a strip matches.
Root.activationMode'automatic' | 'manual''automatic'Automatic selects on arrow; manual waits for Enter.