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

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.

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

<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 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