Tabs
A set of layered sections of content, known as tab panels.
Make changes to your account here. Click save when you're done.
Change your password. After saving, you'll be logged out.
Tabs selects one panel among several, all preloaded in the DOM. TabsList holds the triggers, TabsContent the panels. Only one TabsContent is visible at a time (the others carry hidden).
Good for: grouped preferences, multi-view dashboards, docs (Preview / Code). Bad for: navigation between pages (use NavigationMenu).
With orientation="vertical" the list is a column placed next to the panels, and Arrow Up and Arrow Down move between the tabs.
Installation
$ bext ui add tabs
Copy and paste the following into src/components/ui/tabs.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="tabs" hooks.
Usage
Anatomy
<Tabs defaultValue="a"> ├── <TabsList> │ ├── <TabsTrigger value="a">A</TabsTrigger> │ └── <TabsTrigger value="b">B</TabsTrigger> ├── <TabsContent value="a">Panel A</TabsContent> └── <TabsContent value="b">Panel B</TabsContent>
API Reference
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string | — | Value of the tab selected on load. Applied by the island; defaults to the first trigger. |
| tabs | { value: string; label: string; content: ReactNode }[] | — | Shorthand: generates the list, the triggers and the panels, with the default tab already selected in the HTML. |
| orientation | "horizontal" | "vertical" | "horizontal" | "vertical" lays the list out as a column beside the panels and switches the arrow keys to Up and Down. Written to data-orientation on the root. The shorthand passes it to the list and the panels. |
| id | string | — | id of the root element. In the shorthand it also prefixes the ids that link each tab to its panel, so the link is in the HTML. |
| listClass / listStyle | string | — | Shorthand only: classes and inline style of the generated TabsList. |
| className | string | — | Extra classes on the root. |
TabsList
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | — | The TabsTriggers. Rendered with role="tablist". |
| orientation | "horizontal" | "vertical" | "horizontal" | Composed form: "vertical" writes aria-orientation="vertical" and the column layout in the HTML. When omitted, the island copies the orientation of the Tabs root on load. |
TabsTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Must match the value of a TabsContent. |
| active | boolean | false | Renders the trigger as selected in the HTML, so the right tab shows before the island runs. |
| id | string | — | id of the tab. Generated by the island when omitted. |
| controls | string | — | id of the matching TabsContent, written to aria-controls. Set by the island when omitted. |
| children | ReactNode | — | Tab label. |
TabsContent
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Must match the value of a TabsTrigger. |
| active | boolean | false | Renders the panel visible in the HTML. Without it the panel starts hidden and the island reveals the default one. |
| id | string | — | id of the panel. Generated by the island when omitted. |
| labelledby | string | — | id of the matching TabsTrigger, written to aria-labelledby. Set by the island when omitted. |
| orientation | "horizontal" | "vertical" | "horizontal" | Composed form: "vertical" removes the top margin and lets the panel fill the space beside the list. When omitted, the island copies the orientation of the Tabs root on load. |
| children | ReactNode | — | Panel content. |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="tabs" | Tabs root | — | Scope of one tab set. Also carries data-default-value. |
| data-ui="tab-trigger" | TabsTrigger | — | Clicking selects the tab. |
| data-ui="tab-content" | TabsContent | hidden attribute present or absent | Only the selected panel is shown. |
| data-state | TabsTrigger | "active" | "inactive" | Selection state, updated by the island together with aria-selected and tabindex. |
| data-orientation | Tabs root, TabsList, TabsContent | "vertical" or absent | Vertical layout. On the root it also tells the island which arrow keys to use. |
| aria-orientation | TabsList | "vertical" or absent | Announces a vertical tab list. Absent means horizontal, the default of the tablist role. |
| data-tab | TabsTrigger, TabsContent | the value prop | Links a trigger to its panel. |
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus into the tab list, onto the selected tab. Tab again moves to the visible panel. |
| Arrow Right / Arrow Left | Horizontal list: moves to the next or previous tab and selects it. Wraps at both ends. Not handled in a vertical list. |
| Arrow Down / Arrow Up | Vertical list: moves to the next or previous tab and selects it. Wraps at both ends. Not handled in a horizontal list. |
| Home / End | Moves to the first or last tab and selects it. |
| Enter / Space | Selects the focused tab. |
Accessibility
TabsList sets role="tablist", TabsTrigger sets role="tab" and aria-selected, and TabsContent sets role="tabpanel" and tabindex="0", so a panel with no focusable content can still be reached. Each tab points at its panel with aria-controls and each panel is named by its tab with aria-labelledby. The shorthand writes those ids in the HTML when the root has an id; otherwise the island generates them on load.
The island keeps a roving tabindex: only the selected tab is in the tab order, and the arrow keys move between tabs. Selection follows focus (automatic activation), which suits panels that are already in the page. A horizontal list (the default) answers to Arrow Left and Arrow Right. With orientation="vertical" the list carries aria-orientation="vertical" and answers to Arrow Up and Arrow Down instead, which leaves Arrow Left and Arrow Right free.
The same behaviour applies to a hand-written tab set that only carries the hooks (data-ui="tabs", data-ui="tab-trigger", data-ui="tab-content" with matching data-tab values). Such a set is horizontal unless its root carries data-orientation="vertical".
Examples
Vertical
Name, photo and public description.
Choose which emails you receive.
Invoices and payment method.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page