Headless / Disclosure

Tabs

Tabs switches between sibling panels — a row of tabs, exactly one panel visible at a time, the arrow keys moving between them.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A compound component with four parts. Tabs.Root owns the active value. Tabs.List is the role="tablist" row. Tabs.Tab is one role="tab" button. Tabs.Panel is the matching role="tabpanel". A tab and its panel are linked by their shared value.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { Tabs } from '@usemotif/headless';

Anatomy

example.tsx
<Tabs.Root defaultValue="account">
<Tabs.List>
  <Tabs.Tab value="account">Account</Tabs.Tab>
  <Tabs.Tab value="security">Security</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="account"><AccountForm /></Tabs.Panel>
<Tabs.Panel value="security"><SecurityForm /></Tabs.Panel>
</Tabs.Root>

API

Tabs.Root

Tabs.Root

stable
function Tabs.Root(props: TabsRootProps): JSX.Element
valuestring

Controlled active-tab value. Pass alongside `onValueChange`.

defaultValuestring

Initial active-tab value for the uncontrolled mode.

onValueChange(value: string) => void

Called when the active tab changes.

orientation"horizontal" | "vertical"= "horizontal"

Tab-list orientation. Decides whether the left/right or up/down arrows move between tabs.

childrenReactNode

The List and Panel parts.

Tabs.List

The role="tablist" container. Takes children and style; aria-orientation is set from the Root's orientation.

Tabs.Tab

Tabs.Tab

stable
function Tabs.Tab(props: TabsTabProps): JSX.Element
valuestringrequired

Links this tab to the panel with the same value.

disabledboolean= false

Skip the tab in arrow-key navigation and ignore activation.

styleCSSProperties

Inline style for the tab button.

childrenReactNode

The tab label.

Tabs.Panel

Tabs.Panel

stable
function Tabs.Panel(props: TabsPanelProps): JSX.Element | null
valuestringrequired

Links this panel to the tab with the same value.

forceMountboolean= false

Keep the panel mounted when inactive — hidden via the `hidden` attribute. Use it to preserve panel state across tab switches.

childrenReactNode

The panel content.

styleCSSProperties

Inline style for the panel.

Keyboard

KeyAction
Arrow Left / Right (horizontal)Move to the previous / next tab, wrapping
Arrow Up / Down (vertical)Move to the previous / next tab, wrapping
Home / EndMove to the first / last tab

Moving to a tab both focuses and activates it — the matching panel shows immediately.

Accessibility

Tabs implements the WAI-ARIA tabs pattern. The list is role="tablist", each tab is role="tab" with aria-selected, each panel is role="tabpanel" labelled by its tab. Only the active tab is in the Tab order — tabIndex is 0 on it and -1 on the rest — so a keyboard user tabs into the list once, then uses the arrow keys, and tabs straight out to the panel.

By default an inactive panel is unmounted. forceMount keeps it mounted with the hidden attribute, which preserves its internal state — a half-filled form, a scroll position — across tab switches.

Examples

A settings panel:

example.tsx
<Tabs.Root defaultValue="account">
<Tabs.List style={{ display: 'flex', gap: 4 }}>
  <Tabs.Tab value="account">Account</Tabs.Tab>
  <Tabs.Tab value="security">Security</Tabs.Tab>
  <Tabs.Tab value="billing">Billing</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="account"><AccountForm /></Tabs.Panel>
<Tabs.Panel value="security"><SecurityForm /></Tabs.Panel>
<Tabs.Panel value="billing"><BillingForm /></Tabs.Panel>
</Tabs.Root>