Skip to main content
Gremorie

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 tab

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>
  );
}

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.

PropTypeDefaultDescription
defaultValuestring-Uncontrolled initial active tab.
valuestring-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.

PropTypeDefaultDescription
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.
loopbooleantrueWhether keyboard focus loops at the ends.

<TabsTrigger>

The clickable tab. Wraps Radix Tabs.Trigger.

PropTypeDefaultDescription
valuestring-Required. Matches the corresponding <TabsContent> value.
disabledbooleanfalseDisables this trigger.

<TabsContent>

The panel revealed when its trigger is active. Wraps Radix Tabs.Content.

PropTypeDefaultDescription
valuestring-Required. Matches a <TabsTrigger> value.
forceMountbooleanfalseForce the panel into the DOM even when inactive (useful for animations).

Composition

  1. <Tabs> owns the active value (controlled or uncontrolled).
  2. <TabsList> wraps the triggers. The variant prop lives here, not on Root.
  3. One or more <TabsTrigger> with unique value props go inside the list.
  4. <TabsContent> panels are siblings of the list (not inside it), each with a value that 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

Update your name, email, and avatar.
'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

Your public profile details.
'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

High-level summary of the workspace.
'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 as role="tab" with aria-selected and aria-controls, each panel as role="tabpanel" with aria-labelledby.
  • Keyboard: Tab enters the list and lands on the active trigger; ArrowLeft / ArrowRight (or ArrowUp / ArrowDown when vertical) navigate between triggers; Home / End jump to first / last; Tab again leaves the list and lands on the active panel.
  • Activation mode: with activationMode="automatic" (default), focusing a trigger activates its panel. Set activationMode="manual" when panels carry heavy content and you want users to press Enter or Space to 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-none and relies on its children to manage focus.
  • 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.

On this page