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
'use client';import { Tabs, TabsContent, TabsList, TabsTrigger,} from '@gremorie/rx-navigation';export function TabsPreview() { return ( <Tabs defaultValue="react" className="w-full max-w-md"> <TabsList> <TabsTrigger value="react">React</TabsTrigger> <TabsTrigger value="angular">Angular</TabsTrigger> <TabsTrigger value="tokens">Tokens</TabsTrigger> </TabsList> <TabsContent value="react"> <p className="mt-4 text-sm text-muted-foreground"> Install: <code>npx gremorie add rx-tabs</code> </p> </TabsContent> <TabsContent value="angular"> <p className="mt-4 text-sm text-muted-foreground"> Install: <code>npx gremorie add ng-tabs</code> </p> </TabsContent> <TabsContent value="tokens"> <p className="mt-4 text-sm text-muted-foreground"> Tokens are framework-agnostic - see the Tokens tab. </p> </TabsContent> </Tabs> );}Anatomy
Tabs the Radix root; carries value / orientation
├─ TabsList the trigger track; variant default (pill) or line
│ └─ TabsTrigger one selectable tab
└─ TabsContent the panel shown for the active tabInstallation
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>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
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
'use client';import { Tabs, TabsContent, TabsList, TabsTrigger,} from '@gremorie/rx-navigation';export function TabsVerticalPreview() { return ( <Tabs defaultValue="account" orientation="vertical" className="w-full max-w-md flex-row" > <TabsList> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="security">Security</TabsTrigger> <TabsTrigger value="notifications">Notifications</TabsTrigger> </TabsList> <TabsContent value="account" className="text-sm text-muted-foreground"> Update your name, email, and avatar. </TabsContent> <TabsContent value="security" className="text-sm text-muted-foreground"> Manage password and two-factor authentication. </TabsContent> <TabsContent value="notifications" className="text-sm text-muted-foreground" > Choose which emails and alerts you receive. </TabsContent> </Tabs> );}Set orientation="vertical" and lay the list and panel side by side with flex-row. Use for in-panel settings menus. Arrow Up / Down navigate between triggers when vertical.
With icons
'use client';import { Tabs, TabsContent, TabsList, TabsTrigger,} from '@gremorie/rx-navigation';import { BellIcon, CreditCardIcon, UserIcon } from 'lucide-react';export function TabsWithIconsPreview() { return ( <Tabs defaultValue="profile" className="w-full max-w-md"> <TabsList> <TabsTrigger value="profile"> <UserIcon /> Profile </TabsTrigger> <TabsTrigger value="billing"> <CreditCardIcon /> Billing </TabsTrigger> <TabsTrigger value="alerts"> <BellIcon /> Alerts </TabsTrigger> </TabsList> <TabsContent value="profile" className="mt-4 text-sm text-muted-foreground" > Your public profile details. </TabsContent> <TabsContent value="billing" className="mt-4 text-sm text-muted-foreground" > Plan, invoices, and payment method. </TabsContent> <TabsContent value="alerts" className="mt-4 text-sm text-muted-foreground" > Notification preferences. </TabsContent> </Tabs> );}Drop a lucide-react icon before the label inside each trigger. The trigger styles size and space the icon automatically.
Disabled tab
'use client';import { Tabs, TabsContent, TabsList, TabsTrigger,} from '@gremorie/rx-navigation';export function TabsDisabledPreview() { return ( <Tabs defaultValue="overview" className="w-full max-w-md"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="analytics" disabled> Analytics </TabsTrigger> <TabsTrigger value="reports">Reports</TabsTrigger> </TabsList> <TabsContent value="overview" className="mt-4 text-sm text-muted-foreground" > High-level summary of the workspace. </TabsContent> <TabsContent value="reports" className="mt-4 text-sm text-muted-foreground" > Scheduled and saved reports. </TabsContent> </Tabs> );}Pass disabled on a <TabsTrigger> to render it inert. Disabled triggers are skipped by arrow-key navigation and dimmed.
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.