Tabs
Sibling content panels that share a context. Pill or line variant, horizontal or vertical, with full keyboard navigation.
Overview
Tabs is the canonical sibling-content switcher: one container, several mutually exclusive views inside it. Built on Radix Tabs, it ships two list looks via the variant prop on TabsList - a pill default (rounded background) and a minimal line (single underline on the active trigger).
Reach for Tabs when the views live in the same scope - Profile / Billing / API keys, or Code / Preview for a single example. For cross-section navigation (different pages, different areas of an app), use NavigationMenu or Sidebar instead. For filters that combine, use ToggleGroup.
Preview
Install: npx gremorie add rx-tabs
Installation
bash npx gremorie@latest add rx-tabs bash pnpm dlx gremorie@latest add rx-tabs bash yarn dlx gremorie@latest add rx-tabs bash bunx --bun gremorie@latest add rx-tabs Usage
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@gremorie/rx-navigation";
export function AccountTabs() {
return (
<Tabs defaultValue="profile">
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
<TabsTrigger value="api">API keys</TabsTrigger>
</TabsList>
<TabsContent value="profile">Profile fields...</TabsContent>
<TabsContent value="billing">Billing details...</TabsContent>
<TabsContent value="api">API keys list...</TabsContent>
</Tabs>
);
}Angular edition planned for Phase 5h. Star the repo to track progress.
API
<Tabs>
Root container. Wraps Radix Tabs.Root.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | string | - | Uncontrolled initial active tab. |
value | string | - | Controlled active tab. |
onValueChange | (value: string) => void | - | Fires when the active tab changes. |
orientation | "horizontal" | "vertical" | "horizontal" | Affects keyboard navigation and layout. Vertical lays the list and panel side-by-side. |
dir | "ltr" | "rtl" | "ltr" | Reading direction for arrow-key navigation. |
activationMode | "automatic" | "manual" | "automatic" | "automatic" activates a tab on focus; "manual" requires Enter or Space. |
<TabsList>
Container for the triggers. Wraps Radix Tabs.List.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "line" | "default" | "default" is a pill list with a rounded muted background. "line" is transparent with a single bottom underline on the active trigger. |
loop | boolean | true | Whether keyboard focus loops at the ends. |
<TabsTrigger>
The clickable tab. Wraps Radix Tabs.Trigger.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Required. Matches the corresponding <TabsContent> value. |
disabled | boolean | false | Disables this trigger. |
<TabsContent>
The panel revealed when its trigger is active. Wraps Radix Tabs.Content.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Required. Matches a <TabsTrigger> value. |
forceMount | boolean | false | Force the panel into the DOM even when inactive (useful for animations). |
Composition
<Tabs>owns the active value (controlled or uncontrolled).<TabsList>wraps the triggers. Thevariantprop lives here, not on Root.- One or more
<TabsTrigger>with uniquevalueprops go inside the list. <TabsContent>panels are siblings of the list (not inside it), each with avaluethat matches a trigger.
Render triggers inside <TabsList> only - placing them outside breaks the keyboard navigation contract.
Variations
Pill list (default)
Profile fields...
<Tabs defaultValue="profile">
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
<TabsTrigger value="api">API keys</TabsTrigger>
</TabsList>
<TabsContent value="profile">Profile fields...</TabsContent>
<TabsContent value="billing">Billing details...</TabsContent>
<TabsContent value="api">API keys list...</TabsContent>
</Tabs>Use when tabs sit inside a card or panel and need a clear, contained look.
Line list
High-level summary.
<Tabs defaultValue="overview">
<TabsList variant="line">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="usage">Usage</TabsTrigger>
<TabsTrigger value="api">API</TabsTrigger>
</TabsList>
<TabsContent value="overview">High-level summary.</TabsContent>
<TabsContent value="usage">Code patterns.</TabsContent>
<TabsContent value="api">Prop tables.</TabsContent>
</Tabs>Use for page-level tabs where the surrounding chrome is already heavy and a pill would compete visually.
Vertical orientation
<Tabs defaultValue="account" orientation="vertical" className="flex-row">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="security">Security</TabsTrigger>
<TabsTrigger value="notifications">Notifications</TabsTrigger>
</TabsList>
<TabsContent value="account">...</TabsContent>
<TabsContent value="security">...</TabsContent>
<TabsContent value="notifications">...</TabsContent>
</Tabs>Use for in-panel settings menus. Arrow Up / Down navigate between triggers when orientation="vertical".
Controlled value
const [tab, setTab] = useState('profile');
<Tabs value={tab} onValueChange={setTab}>
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="profile">...</TabsContent>
<TabsContent value="billing">...</TabsContent>
</Tabs>;Use when the active tab must sync with URL state, a router, or a form wizard.
Accessibility
- WAI-ARIA Tabs pattern: the list renders as
role="tablist", each trigger asrole="tab"witharia-selectedandaria-controls, each panel asrole="tabpanel"witharia-labelledby. - Keyboard:
Tabenters the list and lands on the active trigger;ArrowLeft/ArrowRight(orArrowUp/ArrowDownwhen vertical) navigate between triggers;Home/Endjump to first / last;Tabagain leaves the list and lands on the active panel. - Activation mode: with
activationMode="automatic"(default), focusing a trigger activates its panel. SetactivationMode="manual"when panels carry heavy content and you want users to pressEnterorSpaceto commit. - Disabled triggers: render with
aria-disabled="true"and are skipped by arrow-key navigation. - Focus visible: the active trigger keeps a visible focus ring; the panel itself uses
outline-noneand relies on its children to manage focus.
Related
- NavigationMenu - cross-section navigation with rich panels, not sibling content.
- Sidebar - app-shell navigation across many areas.
- ToggleGroup - mutually exclusive choices that aren't view containers.