Tabs
A set of layered sections of content, known as tab panels, that are displayed one at a time.
Installation
Install the following dependencies:
Set up the token layer and the StyleX compiler, if you have not already. See
Installation, and paste states.ts and state-styles.ts
from the same page. states.ts holds a trigger's focus ring, and a trigger
reads its Base UI state through state-styles.ts. Tabs also need
interop.css, which sizes an icon placed inside a trigger.
Copy and paste the following code into your project.
Update the import paths to match your project setup.
Usage
A trigger and its panel are paired by value. Use defaultValue for an
uncontrolled group, or value and onValueChange to control it. A trigger that
carries no value falls back to its index, which is why defaultValue is 0.
Composition
TabsList renders the indicator itself, so there is no TabsIndicator part to
place. The default variant renders two: a pill that slides between tabs, and a
copy of the tab row clipped to the active tab. The copy cross-fades the active
label's colour under the moving pill.
Line
Use variant="line" on TabsList to swap the pill for a 2px bar and drop the
list's fill.
Vertical
Use orientation="vertical" on Tabs to stack the triggers and put the panels
beside them. The line bar moves to the list's inline end.
Disabled
Use the disabled prop on TabsTrigger to dim a tab and stop it activating.
Icons
Put the icon inside the trigger and set iconPosition to the side it sits on.
The tab tightens that padding by 4px so the glyph is not inset as far as a
label would be.
A panel is unmounted while its tab is inactive, so anything typed into it is
lost when you switch back. Pass keepMounted to TabsContent to keep the DOM, or
lift the value into state above Tabs.
API reference
Pass render to replace a part's element or compose it with another component.
Every part wraps the Base UI Tabs part of the same name and accepts its props.
See the Base UI reference. Each
part also takes xstyle and className. TabsTrigger takes style too. It is
real inline CSS, merged over whatever its Base UI state resolves to.
Tabs
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
defaultValue | any | 0 |
value | any | — |
onValueChange | (value, event) => void | — |
xstyle | StyleXStyles | — |
className | string | — |
The root publishes orientation through context, because StyleX has no ancestor
selector for the list and the triggers to read it from the DOM.
TabsList
| Prop | Type | Default |
|---|---|---|
variant | "default" | "line" | "default" |
activateOnFocus | boolean | false |
loopFocus | boolean | true |
TabsTrigger
| Prop | Type | Default |
|---|---|---|
value | any | index |
iconPosition | "start" | "end" | "both" | — |
disabled | boolean | false |
nativeButton | boolean | true |
style | CSSProperties | — |
TabsContent
| Prop | Type | Default |
|---|---|---|
value | any | index |
keepMounted | boolean | false |
tabsStyles
The style maps behind the component. Spread them onto any element.
| Key | What it is |
|---|---|
list | The row: shape, inset, fill, idle colour |
variants | The default well and line list styles, by name |
trigger | The shared focus ring and one tab's box, in order |
trigger is two styles, states.focusRing from theme/states.ts and the tab's
own box. The ring comes first, because a tab sets its own outline-style for the
1px line it draws inside the halo, and a shared style takes a property with every one of its
conditions.
Data attributes
| Attribute | On | Effect |
|---|---|---|
data-slot="tabs" | the root | Targets the group from a parent |
data-slot="tabs-list" | the row | Where the indicator variables are written |
data-slot="tabs-trigger" | a tab | Targets one tab |
data-slot="tabs-content" | a panel | Targets one panel |
data-slot="tabs-indicator" | the clip or bar | The overlay in default, the bar in line |
data-slot="tabs-indicator-pill" | the pill | The box that slides, in default only |
data-slot="tabs-indicator-tab" | a scenery label | A tab's copy inside the clip layer |
data-variant | the row | Mirrors the variant prop |
data-orientation | every part | Mirrors the orientation prop |
data-active, data-disabled | a tab | Base UI writes them, during SSR too |
data-hidden, data-index | a panel | Base UI writes them |
CSS variables
| Variable | On | Effect |
|---|---|---|
--tabs-indicator-radius | TabsList | Rounds the pill and the clip together |
--tabs-indicator-color | TabsList | The line bar's colour |
--active-tab-left, -top | the list | Base UI measures them; the pill and the clip transition to them |
--active-tab-width, -height | the list | The active tab's box, same source |
Set the two --tabs-* properties with style on TabsList. They are plain
custom properties, so they inherit to the pill and the clip layer inside it.