Tabs

A set of layered sections where one panel is shown at a time, selected from a row of tabs, from @clerk/headless. It is a headless primitive: it supplies behavior, active-tab state, roving-tabindex keyboard navigation, an optional position-tracking indicator, and ARIA wiring, but ships no styles — you bring your own CSS by targeting the data-* attributes each part emits.

Example

The demo below is intentionally unstyled — it renders the raw primitive so you can see its behavior and ARIA wiring. Click a tab (or focus one and use the arrow keys) to swap the visible panel.

Manage your account details.

Usage

import { Tabs } from '@clerk/headless/tabs';

<Tabs.Root defaultValue='tab1'>
  <Tabs.List>
    <Tabs.Tab value='tab1'>Account</Tabs.Tab>
    <Tabs.Tab value='tab2'>Security</Tabs.Tab>
    <Tabs.Indicator />
  </Tabs.List>
  <Tabs.Panel value='tab1'>Account content</Tabs.Panel>
  <Tabs.Panel value='tab2'>Security content</Tabs.Panel>
</Tabs.Root>;

Controlled

const [value, setValue] = useState('tab1');

<Tabs.Root
  value={value}
  onValueChange={setValue}
>
  {/* list + panels */}
</Tabs.Root>;

Parts

PartDefault ElementDescription
Tabs.Rootnone (context)Provides active-value state, orientation, and activation mode
Tabs.List<div>role="tablist" container; sets up roving-tabindex navigation
Tabs.Tab<button>A keyboard-navigable tab trigger inside Tabs.List
Tabs.Trigger<button>Standalone tab trigger for use outside Tabs.List (not roving)
Tabs.Panel<div>role="tabpanel"; hidden via the hidden attribute when inactive
Tabs.Indicator<span>Decorative element that tracks the active tab's position and size

All rendered parts accept a render prop for polymorphic rendering and standard HTML attributes for their default element. Tabs.Root renders no element of its own.

Props

Tabs.Root

PropTypeDefaultDescription
valuestringControlled active tab value
defaultValuestring''Initial active tab (uncontrolled)
onValueChange(value: string) => voidCalled when the active tab changes
orientation'horizontal' | 'vertical''horizontal'Arrow-key navigation axis
activationMode'automatic' | 'manual''automatic'automatic selects on focus; manual on Enter/Space

Tabs.Tab / Tabs.Trigger

PropTypeDefaultDescription
valuestring— (required)Tab identifier; must match a Tabs.Panel value
disabledbooleanDisable the tab (uses aria-disabled)

Tabs.Panel

PropTypeDefaultDescription
valuestring— (required)Must match a Tabs.Tab value
shouldForceMountbooleanKeep the panel in layout flow when inactive (enables enter/exit transitions)

Tabs.List and Tabs.Indicator take no additional props beyond standard HTML attributes for their default element.

Styling

Each part emits data-* attributes you can target with any CSS solution:

AttributeApplies ToDescription
data-selectedTab, TriggerPresent on the active tab
data-disabledTab, TriggerPresent when the tab is disabled
data-hiddenPanelPresent when the panel is not selected
data-openPanelPresent while open (force-mounted panels only)
data-closedPanelPresent while mounted-but-deselected (force-mounted panels only)
data-starting-stylePanelEnter-animation first frame (force-mounted panels only)
data-ending-stylePanelExit-animation frame (force-mounted panels only)

Tabs.Indicator is absolutely positioned and exposes the active tab's geometry as CSS variables (give Tabs.List position: relative):

VariableDescription
--cl-tab-leftActive tab left offset relative to list
--cl-tab-widthActive tab width
--cl-tab-topActive tab top offset relative to list
--cl-tab-heightActive tab height

A force-mounted Tabs.Panel also exposes --cl-tab-transition-direction (1 when moving to a later tab, -1 to an earlier one) for directional slide animations.

.cl-tabs-indicator {
  position: absolute;
  left: var(--cl-tab-left);
  width: var(--cl-tab-width);
  transition: left 200ms ease, width 200ms ease;
}