Tabs
Switch between a few peer views in place, showing one panel at a time.
@garn/ui/tabsOn this page
Default
Two-tab Account / Password switcher.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add tabs- Registry
tabs- Deps
@radix-ui/react-tabslucide-react- Registry deps
utilscounter
Import
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@garn/ui/tabs";Anatomy
The parts this component is built from — compose them to assemble it.
TabsRequired[data-slot="tabs"]State container (Radix Root) — owns the active value and orientation. No visual box of its own.
TabsListRequired[data-slot="tabs-list"]The tablist: a muted pill that holds the triggers (roving focus).
TabsTriggerRequired[data-slot="tabs-trigger"]A tab button; its value selects the matching content. Active tab gets a raised background.
TabsContentRequired[data-slot="tabs-content"]A panel; its value matches a trigger. Only the active panel is shown.
Props
The component's public props and their types.
mount- Description
- Panel mounting. `active` mounts a panel's children on first activation; `eager` renders every panel up front (SEO / measure-on-mount). Per-panel `keepMounted` persists an activated panel.
- Type
TabsMountMode- Default
"active"
size- Description
- Trigger height + text/padding, off the shared control ladder.
- Type
TabsSize- Default
"md"
variant- Description
- The tab-strip look, which swaps the active-indicator geometry (not fill): `underline` (a bar on the strip's trailing edge), `pill` (a raised thumb behind the active tab in a soft track), or `enclosed` (folder/card tabs whose own border is the marker).
- Type
TabsVariant- Default
"underline"
Plus 285 inherited native HTML attributes.
TabsList
indicator- Description
- Render an animated active indicator. Set false to opt out.
- Type
boolean- Default
true
scrollButtons- Description
- Show prev/next scroll buttons flanking the list when it overflows (the pointer affordance — keyboard + trackpad scroll already work). The buttons sit *outside* the tablist so the ARIA structure stays clean; they fade out at the reached end. Compose `TabsScrollButton` by hand for bespoke layouts.
- Type
boolean- Default
false
Plus 282 inherited native HTML attributes.
TabsTrigger
badge- Description
- Trailing badge. A number renders a garn `Counter` matched to the tab's size (e.g. an unread count); a node renders verbatim.
- Type
React.ReactNode
icon- Description
- Leading icon (terse path). Compose children for full control.
- Type
React.ReactNode
Plus 291 inherited native HTML attributes.
TabsContent
keepMounted- Description
- Keep this panel mounted (hidden) once it has been activated, so its form / scroll state survives a tab switch. Overrides the root's `mount="active"` lazy unmount for this panel only.
- Type
boolean- Default
false
Plus 283 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="tabs"]States
| Selector | State | Description |
|---|---|---|
[data-state=active] | active | The selected trigger (raised background + shadow) and its panel. |
[data-state=inactive] | inactive | Unselected triggers/panels. |
:focus-visible | focus-visible | 2px ring + offset on triggers and the focused panel. |
[disabled] | disabled | A disabled trigger dims and won't activate. |
| Property | Token | Tier |
|---|---|---|
listHeight | --garn-control-h-md | semantic · air |
ring | --garn-ringbrandable | semantic |
When to use
Switch between a few peer views in the same context, one panel at a time — settings sections, overview/detail, related lists.
Reach for something else when
- Sequential steps that must be done in order (use a stepper)
- navigating to separate routes/pages (use links / a nav menu)
- content that should all be visible at once
- many (~7+) sections or long content (use a side nav or accordion).
Overview
Tabs switch between a few peer views in the same context, one panel at a time — settings sections, overview/detail, related lists. Tabs owns the active value and orientation, TabsList holds the TabsTriggers (roving focus), and each TabsContent panel shows when its value is active. Use tabs when the views are siblings the user toggles between; for sequential, gated steps use a Stepper, and for primary site navigation use NavigationMenu.
Guidelines
Tabs are for peer views, not steps or pages. They imply equal, independent sections in one place — don't use them for an ordered flow (that's a Stepper) or to navigate to separate URLs (that's navigation). Keep the set small enough to see at once; many tabs that wrap or scroll signal the content wants a different structure.
Use short, parallel labels and a sensible default. One or two words each, in the same grammatical form, with the most important panel active first. Don't bury a required action inside a non-default tab where users may never look.
Respect the keyboard model. Arrow keys move between triggers and the active panel is wired via aria-controls; don't put the only path to critical content behind a tab a keyboard user might skip. Avoid tabs within tabs.
Best practices
- Give each TabsTrigger a
valueand a matching TabsContent. - Keep labels short so the tablist fits on one line.
- Set
defaultValue(uncontrolled) orvalue+onValueChange(controlled).
- Don't use tabs for a linear, ordered flow.
- Don't bury content the user always needs behind a non-default tab.
- Don't overload the tablist — switch to a nav menu or accordion when there are many sections.
Content guidelines
- Use short, parallel tab labels (one or two words) that name each view.
- Order tabs by importance or natural sequence, and keep the default tab first.
Troubleshooting
A tab shows nothing (or the wrong panel) when clicked.
Cause. A TabsTrigger value doesn't match any TabsContent value.
Fix. Pair every trigger with a content panel sharing the same value.
Tabs switch on arrow-key focus before the user commits.
Cause. Default activation is automatic.
Fix. Pass activationMode="manual" to require Enter/Space to activate.
Accessibility
- Role
tablist- ARIA APG
- tabs
- Focus
- Triggers and the active panel show a 2px ring; focus moves list → panel on Tab.
Keyboard
| ArrowLeftArrowRight | Move between tabs (horizontal orientation). |
| ArrowUpArrowDown | Move between tabs (vertical orientation). |
| HomeEnd | Jump to the first / last tab. |
| Tab | Move focus from the tablist into the active panel. |
| EnterSpace | Activate the focused tab (manual activation mode). |
- The list is role=tablist with roving tabindex; each trigger is role=tab with aria-selected.
- Each trigger aria-controls its panel; each panel is role=tabpanel, aria-labelledby its trigger.
- Default activation is automatic (focus selects). Use `activationMode="manual"` to require Enter/Space.