Tabs
A roving-focus tab list with full keyboard navigation (arrow keys, Home, End) and an animated active indicator. Built on Radix Tabs.
Installation
npx shadcn@latest add @wuko/tabsUsage
Tabs is a four-piece compound API: Tabs (Root, state container), TabsList (the row of triggers), TabsTrigger (one per tab, sibling under TabsList), and TabsContent(one per tab, sibling to TabsList). Each TabsTrigger's value matches one TabsContent's value.
Controlled
For full control over the active tab (to sync with URL, server state, or external buttons), pass value and onValueChange. Below, the row of buttons under the tabs sets the active tab from outside the component.
As trigger (asChild)
Pass asChild to TabsTriggerto substitute your own element as the trigger. Radix applies the trigger's props (role, data-state, focus management) to your child. The most common use is URL-driven tabs. Render a Link as the trigger so navigation and tab state stay in sync.
Props
Tabs (Root):
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled active tab value. Pair with onValueChange. Omit (and use defaultValue) for uncontrolled mode. |
| defaultValue | string | — | Initial active tab value when uncontrolled. Ignored when value is provided. |
| onValueChange | (value: string) => void | — | Fires when the active tab changes. Required for controlled mode. |
| orientation | "horizontal" | "vertical" | "horizontal" | Layout direction. Vertical orientation rotates roving-focus arrow keys (Up/Down instead of Left/Right). |
| ...rest | ComponentPropsWithoutRef<typeof Radix.Tabs.Root> | — | Forwarded to the underlying Radix Tabs.Root. |
TabsTrigger:
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Required. The unique value matching one TabsContent's value prop. |
| asChild | boolean | false | When true, renders the consumer's child element as the trigger (Radix Slot pattern). Useful for URL-driven tabs (Next Link, anchor) or custom button components. |
| disabled | boolean | false | Disables the trigger; it is skipped by roving focus. |
| ...rest | ComponentPropsWithoutRef<typeof Radix.Tabs.Trigger> | — | Forwarded to the underlying Radix Tabs.Trigger button. |
TabsList and TabsContentare thin wrappers around their Radix counterparts and accept all of those primitives' props.
Accessibility
- Roving focus: arrow keys (Left/Right for horizontal, Up/Down for vertical), Home (first tab), End (last tab). Tabs not currently focused are skipped by Tab key. Only the active tab is in the tab order, matching the ARIA tabs spec.
- The active TabsContent is reachable via Tab from the active trigger, and gets a
focus-visible:outlinewhen focused so long-content panels are keyboard-accessible. asChilduses Radix's Slot pattern: the consumer's child element receives the trigger's ARIA role (role="tab"),aria-selected,aria-controls, and ref. The substituted element must accept those attributes. Anchors, buttons, and any forwardRef component work.- The active indicator bar is decorative: a CSS
::afterpseudo-element driven bydata-state="active". Not announced by screen readers; the role + aria-selected does the announcing.