Tabs

A tabbed interface with accessible keyboard navigation. Built on Radix UI Tabs.

Account

Update your account details. Changes save when you click the button.

Installation

pnpm
pnpm dlx @hex-core/cli add tabs

Vertical orientation

Stacks tabs in a left-side rail. Arrow-up/down navigate; arrow-left/right do nothing in this orientation.

tsx
<Tabs defaultValue="profile" orientation="vertical" className="flex gap-4">
  <TabsList className="flex h-auto flex-col items-stretch">
    <TabsTrigger value="profile" className="justify-start">Profile</TabsTrigger>
    <TabsTrigger value="billing" className="justify-start">Billing</TabsTrigger>
    <TabsTrigger value="team" className="justify-start">Team</TabsTrigger>
  </TabsList>
  <div className="flex-1">
    <TabsContent value="profile">Profile pane.</TabsContent>
    <TabsContent value="billing">Billing pane.</TabsContent>
    <TabsContent value="team">Team pane.</TabsContent>
  </div>
</Tabs>

Disabled tab

Use `disabled` on a `TabsTrigger` when a section isn't available yet (feature behind a flag, premium-only, etc.). Disabled triggers are skipped in keyboard navigation.

tsx
<Tabs defaultValue="current">
  <TabsList>
    <TabsTrigger value="current">Current plan</TabsTrigger>
    <TabsTrigger value="upgrade">Upgrade</TabsTrigger>
    <TabsTrigger value="enterprise" disabled>
      Enterprise (contact sales)
    </TabsTrigger>
  </TabsList>
  <TabsContent value="current">You're on the Pro plan.</TabsContent>
  <TabsContent value="upgrade">Compare plans here.</TabsContent>
  <TabsContent value="enterprise">—</TabsContent>
</Tabs>

Controlled selection

Hold the active tab in parent state when you need to drive it from outside (deep linking, persisting last-viewed tab, syncing across panes). Pair `value` with `onValueChange`.

tsx
function ControlledTabs() {
  const [tab, setTab] = useState("overview");
  return (
    <Tabs value={tab} onValueChange={setTab}>
      <TabsList>
        <TabsTrigger value="overview">Overview</TabsTrigger>
        <TabsTrigger value="activity">Activity</TabsTrigger>
      </TabsList>
      <TabsContent value="overview">Active: {tab}</TabsContent>
      <TabsContent value="activity">Active: {tab}</TabsContent>
    </Tabs>
  );
}

Manual activation

When switching panels triggers expensive work (network fetch, large render), use `activationMode="manual"` so users arrow through triggers without committing. Selection requires Enter/Space.

tsx
<Tabs defaultValue="daily" activationMode="manual">
  <TabsList>
    <TabsTrigger value="daily">Daily</TabsTrigger>
    <TabsTrigger value="weekly">Weekly</TabsTrigger>
    <TabsTrigger value="monthly">Monthly</TabsTrigger>
  </TabsList>
  <TabsContent value="daily">Daily report (heavy query).</TabsContent>
  <TabsContent value="weekly">Weekly report (heavy query).</TabsContent>
  <TabsContent value="monthly">Monthly report (heavy query).</TabsContent>
</Tabs>

API Reference

PropTypeDefaultDescription
defaultValue
stringDefault active tab value (uncontrolled)
value
stringControlled active tab value. Pair with `onValueChange` to manage selection in parent state.
onValueChange
functionCallback: `(value: string) => void`. Fires after the user selects a tab.
orientation
"horizontal" | "vertical"horizontalLayout axis. `horizontal` stacks tabs left-to-right above content; `vertical` stacks them top-to-bottom in a sidebar configuration. Affects keyboard navigation (arrow-up/down vs left/right).
dir
"ltr" | "rtl"Reading direction. Defaults to inherited locale direction; pass `rtl` explicitly when nesting in a left-to-right parent that should still flip this widget.
activationMode
"automatic" | "manual"automaticHow tabs activate. `automatic` switches the panel as the user arrows through (read-on-hover semantics); `manual` requires Enter/Space to commit the selection — use this when the panel transition is expensive.
className
stringAdditional CSS classes merged onto the root element.

AI Guidance

When to use

Use to organize content into switchable panels: settings pages, dashboards, product details with multiple sections.

When not to use

Don't use for navigation between pages (use router/links). Don't use for steppers/wizards (use a stepper component).

Common mistakes

  • Missing `defaultValue` (or `value`) — no tab is selected initially and the first panel renders empty
  • `TabsTrigger value` not matching `TabsContent value` — the panel never opens because Radix matches them as strings
  • Using Tabs for page navigation — use Next.js routing + `<Link>` instead; Tabs is for in-page content switching only
  • Forgetting to switch to `activationMode="manual"` when each panel kicks off a heavy network fetch — automatic activation will fire fetches as the user arrows past every tab
  • Mixing controlled (`value`) and uncontrolled (`defaultValue`) on the same Tabs root — pick one

Accessibility

Full keyboard navigation is built-in: arrow-left/right (or up/down in vertical orientation) cycle triggers, Home/End jump to the first/last, Enter/Space activate when `activationMode="manual"`. Radix wires `role="tablist"`, `role="tab"`, `role="tabpanel"`, `aria-selected`, and `aria-controls` automatically. When using a vertical layout, set `orientation="vertical"` on the root so screen readers announce the orientation correctly and arrow-key handling matches the visual axis.

Related components

Token budget: 700