bx

Tabs

A set of layered sections of content, known as tab panels.

tsx
<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">
    <div className="rounded-md border p-4 text-sm">
      <div className="font-medium">Account</div>
      <p className="text-muted-foreground">Make changes to your account here. Click save when you're done.</p>
    </div>
  </TabsContent>
  <TabsContent value="password">
    <div className="rounded-md border p-4 text-sm">
      <div className="font-medium">Password</div>
      <p className="text-muted-foreground">Change your password. After saving, you'll be logged out.</p>
    </div>
  </TabsContent>
</Tabs>

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

Terminal
$ bext ui add tabs

Usage

tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@bext-stack/ui/primitives"

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">Account settings...</TabsContent>
  <TabsContent value="password">Password change...</TabsContent>
</Tabs>

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

Profile

Name, photo and public description.

tsx
<Tabs defaultValue="profile" orientation="vertical">
  <TabsList orientation="vertical">
    <TabsTrigger value="profile">Profile</TabsTrigger>
    <TabsTrigger value="notifications">Notifications</TabsTrigger>
    <TabsTrigger value="billing">Billing</TabsTrigger>
  </TabsList>
  <TabsContent value="profile" orientation="vertical">Name, photo and public description.</TabsContent>
  <TabsContent value="notifications" orientation="vertical">Choose which emails you receive.</TabsContent>
  <TabsContent value="billing" orientation="vertical">Invoices and payment method.</TabsContent>
</Tabs>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.