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.
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.
Related
- Card — for grouping content that does not need a view-switcher.
- Separator — to divide sections inside a single tab panel without introducing more tabs.