Tabs
A set of panels where one shows at a time.
Change your name here. You're done when you save.
Pick a new password. You'll be signed out elsewhere.
import { Div, Label, Input, P } from "@implementjs/core";
import { Button } from "@/lib/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/lib/components/ui/tabs";
function Field(id: string, label: string, value: string) {
return Div(
{ class: "grid gap-1.5" },
Label({ for: id, class: "text-sm font-medium" }, label),
Input({
id,
value,
class:
"h-8 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
}),
);
}
export default function TabsDemo() {
return Tabs(
{ value: "account", class: "w-full max-w-sm" },
TabsList(
{ class: "w-full" },
TabsTrigger({ value: "account" }, "Account"),
TabsTrigger({ value: "password" }, "Password"),
),
TabsContent(
{ value: "account", class: "grid gap-3 rounded-lg border p-4" },
P(
{ class: "text-sm text-muted-foreground" },
"Change your name here. You're done when you save.",
),
Field("tabs-demo-name", "Name", "Aidan Bleser"),
Button({ size: "sm", class: "w-fit" }, "Save changes"),
),
TabsContent(
{ value: "password", class: "grid gap-3 rounded-lg border p-4" },
P(
{ class: "text-sm text-muted-foreground" },
"Pick a new password. You'll be signed out elsewhere.",
),
Field("tabs-demo-password", "New password", ""),
Button({ size: "sm", class: "w-fit" }, "Save password"),
),
);
}Tabs split one region into layers the reader switches between — settings sections, a preview next to its source. Tabs is the root and owns the selected value, TabsList holds the triggers, and each TabsContent is the panel for one value.
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@implementjs/primitives";
Tabs(
{ value: "account" },
TabsList(
TabsTrigger({ value: "account" }, "Account"),
TabsTrigger({ value: "password" }, "Password"),
),
TabsContent({ value: "account" }, "Change your name here."),
TabsContent({ value: "password" }, "Pick a new password here."),
);
Each part accepts optional props and children — pass a props object when you need attributes, or pass children directly. See createComponent. Extra props are forwarded onto the underlying Div or Button.
A trigger and its panel are paired by value, and the pairing is what wires aria-controls and aria-labelledby between them — so the two must match exactly.
Value
value is the selected tab. Pass a signal to control it from outside — reading it tells you which tab is showing, and setting it switches tabs without a click:
const tab = signal("account");
Tabs(
{ value: tab },
TabsList(TabsTrigger({ value: "account" }, "Account")),
TabsContent({ value: "account" }, "Change your name here."),
);
A plain string seeds uncontrolled state instead. Omitting it starts with nothing selected: every panel is hidden and every trigger is reachable with Tab until one is picked.
Activation
activationMode (default "automatic") selects a tab as soon as its trigger is focused, so arrowing through the list swaps panels as you go. Use "manual" when a panel is expensive to show — arrow keys then only move focus, and Space, Enter, or a click selects.
Tabs({ value: "preview", activationMode: "manual" } /* ... */);
Keyboard and focus
The list is one Tab stop: arrow keys move between the triggers, loop (default true) wraps at the ends, and Home and End jump to them. orientation (default "horizontal") picks which arrows move — Left/Right when horizontal, Up/Down when vertical. Disabled triggers are skipped.
Each panel is itself focusable (tabindex="0"), so Tab out of the list lands on the content even when it holds no focusable elements.
Disabled
Pass disabled on the root to disable every trigger, or on one trigger to disable just it. Both accept a signal, set the native disabled attribute, and add data-disabled for styling.
Accessibility
The list is role="tablist" with aria-orientation, each trigger is role="tab" with aria-selected and aria-controls, and each panel is role="tabpanel" labelled by its trigger. Name the list with aria-label or aria-labelledby when the page has more than one.
Hidden panels use the hidden attribute, so their content stays out of the accessibility tree and out of find-in-page.
Styling
The parts set data-tabs-root, data-tabs-list, data-tabs-trigger, and data-tabs-content. Triggers and panels carry data-state as "active" or "inactive", plus data-value, data-orientation, and data-disabled:
TabsTrigger({
value: "account",
class: "rounded-md px-2 py-1 text-sm data-[state=active]:bg-background",
});
API Reference
Tabs
The root. Owns the selected value, the activation mode, and the arrow-key focus movement. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | Signal<string> | string | "" | The selected tab. Pass a signal to control it from outside; a string seeds uncontrolled state. Empty means nothing is selected. |
orientation | "horizontal" | "vertical" | "horizontal" | Which arrow keys move focus, and the data-orientation attributes. |
loop | boolean | true | Whether arrow keys wrap from the last trigger back to the first. |
activationMode | "automatic" | "manual" | "automatic" | Whether focusing a trigger selects its tab, or selection waits for a click or Space/Enter. |
disabled | Signal<boolean> | boolean | false | Disables every trigger. |
| Data attribute | Value |
|---|---|
[data-tabs-root] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
TabsList
Holds the triggers. Sets role="tablist" and aria-orientation. Renders a Div; extra props are forwarded onto it.
| Data attribute | Value |
|---|---|
[data-tabs-list] | Present |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when the root is disabled |
TabsTrigger
One tab. Sets role="tab", aria-selected, and aria-controls pointing at the matching content. Renders a Button; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifies the tab. Must match the TabsContent it opens. |
id | string | — | Defaults to a generated id. The matching panel points back at it with aria-labelledby. |
disabled | Signal<boolean> | boolean | false | Prevents selecting the tab. Sets disabled and data-disabled. |
| Data attribute | Value |
|---|---|
[data-tabs-trigger] | Present |
[data-state] | "active" | "inactive" |
[data-value] | The tab's value |
[data-orientation] | "horizontal" | "vertical" |
[data-disabled] | Present when disabled |
TabsContent
The panel for one tab. Sets role="tabpanel", is labelled by its trigger, and is hidden unless selected. Renders a Div; extra props are forwarded onto it.
| Prop | Type | Default | Description |
|---|---|---|---|
value* | string | — | Identifies the panel. Must match the TabsTrigger that opens it. |
id | string | — | Defaults to a generated id. The matching trigger points at it with aria-controls. |
| Data attribute | Value |
|---|---|
[data-tabs-content] | Present |
[data-state] | "active" | "inactive" |
[data-value] | The tab's value |
[data-orientation] | "horizontal" | "vertical" |