Tabs

Switch between sibling panels of content that share the same surface, without navigating away.

Tabs is a thin wrapper around Radix’s tabs primitive. It lets the user pick one of several mutually exclusive views that belong to the same surface, a settings pane split into sections, an API resource shown as “Overview”, “Keys”, and “Logs”, or a code sample offered in several languages. Only one panel is rendered at a time, and the choice is preserved as internal state (or lifted to the parent when controlled).

Reach for Tabs when the options are peers of the same thing and the user may want to flip between them. If the panels represent distinct pages, use navigation instead, tabs do not update the URL by default and do not create browser history. If the panels are genuinely independent and the user might want to see them side by side, use a grid of cards.

Import

import {
  Tabs,
  TabsList,
  TabsTrigger,
  TabsContent,
} from "@unkey/ui";

<Tabs> owns the selected value and distributes it through React context. <TabsList> groups the triggers for keyboard navigation, each <TabsTrigger value="..."> activates the panel whose <TabsContent value="..."> matches.

Basic

Set a defaultValue on <Tabs> and pair each trigger with a content panel using the same value. The component manages the active tab internally.

2,431 keys issued against the ACME production API today.

Controlled

Pass value and onValueChange to lift the active tab into the parent. Use this when the selection needs to be read or set from elsewhere, for example syncing to a URL query parameter, restoring a tab from localStorage, or driving the tabs from a form.

Active: overview

The parent owns the active tab; changing state from outside also updates the tabs.

Orientation

orientation="vertical" switches Radix’s arrow-key handling from left/right to up/down, which suits settings panes and long option lists. The default TabsList renders as a horizontal flex row, so override its layout classes (flex-col, h-auto, items-stretch) when rendering a vertical column.

Vertical tabs suit settings panes and dense navigation where a horizontal row would wrap.

Accessibility

Radix handles the ARIA wiring. <TabsList> is a role="tablist", each <TabsTrigger> is a role="tab" with aria-selected and aria-controls pointing at its panel, and each <TabsContent> is a role="tabpanel" with aria-labelledby pointing back at its trigger. The active tab is the only focusable trigger, so Tab moves focus to the tablist and then out again, not across every trigger.

Inside the tablist, arrow keys cycle through triggers (left/right for horizontal, up/down for vertical), Home jumps to the first, End to the last. By default, focusing a trigger also activates it (automatic activation); set activationMode="manual" on <Tabs> if activating a panel is expensive and you want the user to press Enter or Space explicitly. Disable a trigger with disabled and Radix removes it from the keyboard cycle.

Props

Tabs

The root state container. Owns the active value and distributes it to triggers and panels via React context, so every Tabs tree must be rendered as a single component (do not split triggers and content across Astro boundaries).

defaultValue string Optional

Value of the tab selected on first render. Use for uncontrolled usage.

value string Optional

Controlled active value. Pair with onValueChange when the parent needs to read or set the current tab.

onValueChange (value: string) => void Optional

Fires whenever the active tab changes, whether by click, keyboard, or programmatic control.

orientation "horizontal" | "vertical" Optional Default "horizontal"

Direction of arrow-key navigation inside the tablist. Does not restyle the list, override TabsList classes to render vertically.

dir "ltr" | "rtl" Optional

Reading direction. Flips the left/right arrow-key bindings when set to rtl. Inherits from the nearest Radix direction provider when omitted.

activationMode "automatic" | "manual" Optional Default "automatic"

When automatic, focusing a trigger activates it. When manual, the user must press Enter or Space to activate. Use manual for expensive panels.

children ReactNode Optional

Typically one <TabsList> followed by one <TabsContent> per trigger.

TabsList

The container for the triggers. Renders as a horizontal pill with a muted background and a light inset. Owns the keyboard navigation for the triggers inside it.

loop boolean Optional Default true

When true, arrow-key navigation wraps from the last trigger back to the first.

className string Optional

Additional Tailwind classes. Merged with the default chrome (h-9, rounded-lg, bg-gray-2, p-1) via cn. Override layout classes (flex-col, h-auto, items-stretch) for a vertical list.

children ReactNode Optional

A flat list of <TabsTrigger> elements.

TabsTrigger

A single tab button. When active, its background is raised to the surface color and its label darkens. Inactive triggers hover-highlight on pointer.

value string Required

Identifier that matches a <TabsContent value="...">. Must be unique inside the <Tabs> tree.

disabled boolean Optional Default false

Grey out the trigger and remove it from the keyboard cycle.

asChild boolean Optional Default false

Merge the trigger’s behavior into its single child instead of rendering a <button>. Use when pairing with a custom element.

className string Optional

Additional Tailwind classes. Merged with the default chrome via cn, the active state is styled through data-[state=active]:* selectors.

children ReactNode Optional

The label content, usually short text, optionally paired with a leading icon.

TabsContent

The panel associated with a trigger. Only the active panel is mounted in the DOM by default, switching tabs unmounts the previous panel and remounts the new one.

value string Required

Identifier that matches a <TabsTrigger value="...">.

forceMount boolean Optional Default false

When true, the panel stays mounted even while inactive. Useful when the panel owns expensive state that should survive tab changes, or when animating between panels.

className string Optional

Additional Tailwind classes. Merged with the default mt-2 spacing via cn, override with mt-0 when laying the content out beside a vertical TabsList.

children ReactNode Optional

The panel content. Can be any React subtree.

  • Card — for grouping content that does not need a view-switcher.
  • Separator — to divide sections inside a single tab panel without introducing more tabs.