Tabs
Switch between a few peer views in place, showing one panel at a time.
@garn/ui/tabsOn this page
Default
Account / Password / Team switcher using the default chip treatment.
Appearance
Tab-strip look. `chip` seats the active tab on a chip above a sliding bar, `underline` is the same strip text-led with the bar alone carrying selection, `segmented` is a soft track with a raised thumb, and `folder` is a folder/card treatment whose active border is the marker.
Tab strip styles
Choose the strip treatment that matches the surrounding surface while keeping the same tab semantics and keyboard behavior.
Sizing
Trigger height, label scale, padding, and icon scale from garn's control ladder.
Spacing
The same control rendered across garn's density modes — compact, default, and spacious.
Orientation
Horizontal tabs place the list above the panel; vertical tabs place the list beside it.
Tone
Neutral (ink) vs brand (accent) selection colour on the default chip strip.
Icons badges
Leading icons and trailing unread counts on triggers.
Overflow
An overflowing tablist with optional scroll buttons and edge fades.
Overflow menu
An overflowing tablist with a trailing ⋯ jump menu that lists and activates any tab.
Keep mounted
A draft panel that preserves its input when switching to Preview.
Related components
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
utilsfocus-ringcounterdropdown-menu
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 that holds the triggers and active marker (roving focus). Its surface follows the root variant.
TabsTriggerRequired[data-slot="tabs-trigger"]A tab button; its value selects the matching content. Active styling follows the root variant.
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.
orientation- Description
- No description.
- Type
"horizontal" | "vertical"- Default
horizontal
size- Description
- Trigger height + text/padding, off the shared control ladder.
- Type
"lg" | "md" | "sm"- Default
"md"
tone- Description
- Colour intent for the selection marker — the active label, and the bar under `chip` / `underline`. `neutral` reads as ink, `brand` as accent.
- Type
"brand" | "neutral"- Default
"neutral"
variant- Description
- The tab-strip look. `chip` seats the active tab on a chip above a bar on the strip's trailing edge; `underline` is the same strip text-led, the bar alone; `segmented` sits a raised thumb behind the active tab in a soft track; `folder` draws card tabs whose active one joins the panel below it.
- Type
"chip" | "folder" | "segmented" | "underline"- Default
"chip"
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"
Plus 284 inherited native HTML attributes.
TabsList
indicator- Description
- Render an animated active indicator. Set false to opt out.
- Type
boolean- Default
true
overflowMenu- Description
- Show a trailing `⋯` menu button (only while the list overflows) that lists every tab; picking one activates it and scrolls it into view. Unlike the tabs themselves it is a real focusable menu button, so it stays reachable when tabs are scrolled off. An alternative to `scrollButtons` for narrow strips; compose `TabsOverflowMenu` by hand for bespoke layouts.
- Type
boolean- Default
false
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.
- Status
- Stable
- Version
v0.5.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history for individual components. This reflects the component's current lifecycle state; a full per-release history is coming.