Tabs component
Tabs is a composable tabbed-navigation component with a roving-tabindex keyboard model built in — arrow keys move between tabs, only the active one sits in the tab order, and panels switch without a page reload. It can be driven declaratively with <Tab> children or dynamically from a data array via a render-function child.
Playground
Change a prop on the right and the preview updates. Switch to Code to copy what you see.
Recipes
Copy-paste patterns for the ways Tabs actually gets used — each one a real, self-contained example.
Basic tabs
Render <Tab> children with a title and panel content. Arrow-key navigation and the tablist/tabpanel ARIA pattern are built in — pass an aria-label describing the set.
<Tabs aria-label="Account settings">
<Tab key="profile" title="Profile">Your public profile.</Tab>
<Tab key="billing" title="Billing">Plans and invoices.</Tab>
<Tab key="security" title="Security">Password and 2FA.</Tab>
</Tabs>Variants
Four visual styles — solid, bordered, light, and underlined — so tabs fit anything from a card header to a full-page section switcher.
<Tabs aria-label="Style: solid" variant="solid">…</Tabs>
<Tabs aria-label="Style: bordered" variant="bordered">…</Tabs>
<Tabs aria-label="Style: light" variant="light">…</Tabs>
<Tabs aria-label="Style: underlined" variant="underlined">…</Tabs>Accent color
color tints the active tab with a semantic tone — pair it with the underlined variant for a clean, branded section switcher.
<Tabs aria-label="Sections" variant="underlined" color="primary">
<Tab key="overview" title="Overview">…</Tab>
<Tab key="activity" title="Activity">…</Tab>
<Tab key="settings" title="Settings">…</Tab>
</Tabs>Vertical tabs
isVertical stacks the tab list; pair with placement="start" for a settings-style sidebar layout.
<Tabs aria-label="Settings" isVertical placement="start">
<Tab key="general" title="General">…</Tab>
<Tab key="notifications" title="Notifications">…</Tab>
<Tab key="advanced" title="Advanced">…</Tab>
</Tabs>Tabs with icons
A title can be any node — an icon beside a label. When the title isn't plain text, pass titleValue so assistive tech still gets a real name.
<Tabs aria-label="Views">
<Tab
key="grid"
title={<span className="flex items-center gap-2"><GridIcon /> Grid</span>}
titleValue="Grid"
>
…
</Tab>
<Tab
key="home"
title={<span className="flex items-center gap-2"><HomeIcon /> Home</span>}
titleValue="Home"
>
…
</Tab>
</Tabs>Declarative tabs
For a fixed, known set of tabs, render <Tab> children directly — each one's title is the label, and its children are the panel content for that tab.
<Tabs aria-label="Account settings">
<Tab title="Profile">
<ProfileForm />
</Tab>
<Tab title="Billing">
<BillingForm />
</Tab>
<Tab title="Security">
<SecurityForm />
</Tab>
</Tabs>Dynamic tabs from data
When tabs come from an array (a list of projects, a set of API-driven categories), pass items and a render-function child instead of hand-writing a <Tab> per entry — the same underlying rendering, just data-driven.
<Tabs aria-label="Projects" items={projects}>
{(project) => (
<Tab key={project.id} title={project.name}>
<ProjectSummary project={project} />
</Tab>
)}
</Tabs>Controlled selection
Tabs manages its own selected tab by default (optionally seeded with defaultSelectedKey). Switch to selectedKey + onSelectionChange when something outside Tabs needs to react to or drive the active tab — syncing it to a URL query param, for instance.
const [tab, setTab] = useState("profile");
<Tabs selectedKey={tab} onSelectionChange={(key) => setTab(String(key))}>
<Tab key="profile" title="Profile">...</Tab>
<Tab key="billing" title="Billing">...</Tab>
</Tabs>Placement and orientation
placement moves the tab list relative to its panels (top, bottom, start, end); isVertical switches the tab list itself to a vertical stack, useful paired with placement="start"/"end" for a sidebar-style tab layout.
<Tabs placement="start" isVertical aria-label="Settings">
<Tab title="General">...</Tab>
<Tab title="Notifications">...</Tab>
</Tabs>Keyboard activation mode
keyboardActivation="automatic" (the default) selects a tab as soon as arrow-key focus reaches it. Switch to "manual" when moving focus shouldn't immediately trigger a (potentially expensive) panel change — the user arrows through tabs freely and only Enter/Space actually selects one.
Props
The full public API for Tabs. Every prop is stable and intentionally typed.
Content
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | ((item: T) => ReactElement) | — | Declarative <Tab> children, or a render function with items. |
| items | Iterable<T> | — | Dynamic items, used with a render-function children. |
| title | ReactNode | — | Tab label (required). |
| titleValue | string | — | Plain-text override of title, used for accessible naming when title is a node. |
| children | ReactNode | — | Panel content for this tab. |
children
ReactNode | ((item: T) => ReactElement)
default: —
Declarative <Tab> children, or a render function with items.
items
Iterable<T>
default: —
Dynamic items, used with a render-function children.
title
ReactNode
default: —
Tab label (required).
titleValue
string
default: —
Plain-text override of title, used for accessible naming when title is a node.
children
ReactNode
default: —
Panel content for this tab.
Appearance
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "solid" | "bordered" | "light" | "underlined" | solid | Visual style variant. |
| color | "default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger" | default | Semantic color. |
| size | "xs" | "xs-sm" | "sm" | "md" | "lg" | "xl" | md | Tab list size. |
| radius | "none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full" | full | Border radius. |
variant
"solid" | "bordered" | "light" | "underlined"
default: solid
Visual style variant.
color
"default" | "primary" | "secondary" | "info" | "success" | "warning" | "danger"
default: default
Semantic color.
size
"xs" | "xs-sm" | "sm" | "md" | "lg" | "xl"
default: md
Tab list size.
radius
"none" | "xs" | "xs-sm" | "sm" | "md" | "lg" | "full"
default: full
Border radius.
Layout & positioning
| Prop | Type | Default | Description |
|---|---|---|---|
| fullWidth | boolean | false | Stretches tabs to fill the available width. |
| placement | "top" | "bottom" | "start" | "end" | top | Tab list position relative to panels. |
| isVertical | boolean | false | Renders a vertical tab list. |
fullWidth
boolean
default: false
Stretches tabs to fill the available width.
placement
"top" | "bottom" | "start" | "end"
default: top
Tab list position relative to panels.
isVertical
boolean
default: false
Renders a vertical tab list.
Behavior & states
| Prop | Type | Default | Description |
|---|---|---|---|
| disabledKeys | Key[] | — | Keys of tabs to disable. |
| selectedKey | Key | — | Controlled selected tab key. |
| defaultSelectedKey | Key | — | Uncontrolled initial selected tab key. |
| shouldSelectOnPressUp | boolean | true | Selects on pointer-up rather than pointer-down. |
| keyboardActivation | "automatic" | "manual" | automatic | Whether arrow-key focus also selects, or only Enter/Space does. |
| isDisabled | boolean | false | Disables the whole tab list. |
| destroyInactiveTabPanel | boolean | false | Unmounts inactive panels instead of hiding them. |
| href / target / rel / download / ping / referrerPolicy | string | — | Renders the tab as a link when href is set. |
| isDisabled | boolean | false | Disables this specific tab. |
| shouldSelectOnPressUp | boolean | — | Per-tab override of the Tabs-level setting. |
disabledKeys
Key[]
default: —
Keys of tabs to disable.
selectedKey
Key
default: —
Controlled selected tab key.
defaultSelectedKey
Key
default: —
Uncontrolled initial selected tab key.
shouldSelectOnPressUp
boolean
default: true
Selects on pointer-up rather than pointer-down.
keyboardActivation
"automatic" | "manual"
default: automatic
Whether arrow-key focus also selects, or only Enter/Space does.
isDisabled
boolean
default: false
Disables the whole tab list.
destroyInactiveTabPanel
boolean
default: false
Unmounts inactive panels instead of hiding them.
href / target / rel / download / ping / referrerPolicy
string
default: —
Renders the tab as a link when href is set.
isDisabled
boolean
default: false
Disables this specific tab.
shouldSelectOnPressUp
boolean
default: —
Per-tab override of the Tabs-level setting.
Motion
| Prop | Type | Default | Description |
|---|---|---|---|
| disableCursorAnimation | boolean | false | Disables the sliding active-tab cursor animation. |
| disableAnimation | boolean | false | Disables all animation, including the cursor. |
disableCursorAnimation
boolean
default: false
Disables the sliding active-tab cursor animation.
disableAnimation
boolean
default: false
Disables all animation, including the cursor.
Events
| Prop | Type | Default | Description |
|---|---|---|---|
| onSelectionChange | (key: Key) => void | — | Called when the selected tab changes. |
onSelectionChange
(key: Key) => void
default: —
Called when the selected tab changes.
Styling
| Prop | Type | Default | Description |
|---|---|---|---|
| classNames | TabsClassNames | — | Per-slot class overrides (base/tabList/tab/tabContent/cursor/panel/tabWrapper). |
| className | string | — | Class applied to the root element. |
classNames
TabsClassNames
default: —
Per-slot class overrides (base/tabList/tab/tabContent/cursor/panel/tabWrapper).
className
string
default: —
Class applied to the root element.
Accessibility
- Implements the standard tablist/tab/tabpanel ARIA pattern with roving tabindex — only the active tab is in the natural tab order; arrow keys move focus between the rest.
- Disabled tabs (isDisabled, either on the whole Tabs or a single Tab) are skipped by arrow-key navigation, not just visually dimmed.
- A Tab's accessible name comes from title; if title is a non-text ReactNode (an icon plus a label, say), pass titleValue with the plain-text equivalent so assistive tech still gets a real name.
- Always pass Tabs its own aria-label (or aria-labelledby) describing what the tab set is for — "Account settings", "Projects" — since the tabs' own titles alone don't convey the group's purpose.
Best practices
- Use Tabs for switching between views of related content within the same context — not as a substitute for real page-level navigation between unrelated sections.
- Set destroyInactiveTabPanel when panels are expensive to keep mounted (a heavy chart, a form with its own side effects); leave it off (the default) when switching back to a tab should preserve its scroll position and in-progress state.
- Prefer the declarative <Tab> children form for a small, fixed set of tabs, and the items + render-function form once the set is genuinely data-driven — trying to force a small static set through the dynamic form just adds indirection.
- Keep tab titles short — a tab list isn't the place for a full sentence, and long titles fight the fixed-width layout most tab bars use.