Tabs
A compound tab component with triggers and content panels
A compound tab component. Declare triggers and content panels side by side — the active panel renders automatically.
Run demo
bunx @gridland/demo tabscurl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s tabsInstallation
bunx create-gridland add tab-barUsage
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tab-bar"<Tabs defaultValue="files">
<TabsList focusId="tabs" autoFocus>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="search">Search</TabsTrigger>
<TabsTrigger value="git">Git</TabsTrigger>
</TabsList>
<TabsContent value="files">
<text>Browse project files</text>
</TabsContent>
<TabsContent value="search">
<text>Search across files</text>
</TabsContent>
<TabsContent value="git">
<text>View git status</text>
</TabsContent>
</Tabs>TabsList is focused-is-interactive: pass focusId and arrow/h/l keys
fire whenever that id is focused (no separate "Enter to select" step needed).
Wrap your app in GridlandProvider or a FocusProvider for the focus system
to route keys.
Examples
Full Example
Simple API
For cases where you don't need content panels, use the TabBar convenience wrapper.
import { TabBar } from "@/components/ui/tab-bar"
<TabBar
focusId="tabs"
autoFocus
label="View"
options={["Files", "Search", "Git"]}
selectedIndex={0}
onValueChange={setSelectedIndex}
/>With Label
Show a text label before the triggers.
<Tabs defaultValue="code">
<TabsList label="Language">
<TabsTrigger value="code">Code</TabsTrigger>
<TabsTrigger value="preview">Preview</TabsTrigger>
</TabsList>
<TabsContent value="code">
<text>Source code</text>
</TabsContent>
<TabsContent value="preview">
<text>Live preview</text>
</TabsContent>
</Tabs>Controlled
Use value and onValueChange to control the active tab externally.
const [tab, setTab] = useState("files")
<Tabs value={tab} onValueChange={setTab}>
<TabsList>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="search">Search</TabsTrigger>
</TabsList>
<TabsContent value="files">
<text>Files panel</text>
</TabsContent>
<TabsContent value="search">
<text>Search panel</text>
</TabsContent>
</Tabs>Unfocused
Set focused={false} on TabsList to render in a dimmed, unfocused style.
<Tabs defaultValue="a">
<TabsList focused={false}>
<TabsTrigger value="a">Tab A</TabsTrigger>
<TabsTrigger value="b">Tab B</TabsTrigger>
</TabsList>
</Tabs>Disabled Tab
Disable individual tabs to prevent keyboard navigation to them.
<Tabs defaultValue="code">
<TabsList>
<TabsTrigger value="code">Code</TabsTrigger>
<TabsTrigger value="preview" disabled>Preview</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent value="code">
<text>Source code</text>
</TabsContent>
<TabsContent value="settings">
<text>Settings panel</text>
</TabsContent>
</Tabs>No Separator
Hide the horizontal separator line below the tab bar.
<Tabs defaultValue="a">
<TabsList separator={false}>
<TabsTrigger value="a">Tab A</TabsTrigger>
<TabsTrigger value="b">Tab B</TabsTrigger>
</TabsList>
</Tabs>Compound Components
| Component | Description |
|---|---|
Tabs | Root container with active tab state |
TabsList | Horizontal tab bar built from TabsTrigger children |
TabsTrigger | Declares a tab option. Does not render on its own — TabsList reads its props. |
TabsContent | Renders its children only when its value matches the active tab |
TabBar | Simple convenience wrapper (no content panels) |
API Reference
Tabs
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Controlled active tab value |
defaultValue | string | "" | Default active tab (uncontrolled) |
onValueChange | (value: string) => void | - | Called when the active tab changes |
children | ReactNode | - | Sub-components |
TabsList
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Text label shown before the triggers |
focused | boolean | true | Whether the tab bar appears focused |
activeColor | string | theme.accent | Foreground color of the active trigger |
separator | boolean | true | Show horizontal separator below triggers |
focusId | string | auto-generated | Stable id for the focus system. Arrow/h/l navigation fires while this id is focused. |
autoFocus | boolean | false | Focus this tab bar on mount |
children | ReactNode | - | TabsTrigger components |
TabsTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Unique value linking this trigger to its TabsContent |
disabled | boolean | false | Disables the tab — skipped during keyboard navigation and rendered dimmed |
children | ReactNode | - | Label text displayed in the tab bar |
TabsContent
| Prop | Type | Description |
|---|---|---|
value | string | Must match the active tab value to render |
children | ReactNode | Content shown when this tab is active |
TabBar
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | - | Text label shown before the options |
options | string[] | - | Array of option strings to display |
selectedIndex | number | - | Zero-based index of the selected option |
focused | boolean | true | Whether the tab bar appears focused |
activeColor | string | theme.accent | Foreground color of the selected option |
separator | boolean | true | Show horizontal separator below tabs |
focusId | string | auto-generated | Stable id for the focus system |
autoFocus | boolean | false | Focus this tab bar on mount |
onValueChange | (index: number) => void | - | Called when the active tab changes via keyboard navigation |
Controls
| Key | Action |
|---|---|
← / h | Previous tab (wraps around) |
→ / l | Next tab (wraps around) |