Skip to content

Tabs

Switch between a few peer views in place, showing one panel at a time.

Stablev0.4.2added in v0.1.0@garn/ui/tabs
On this page

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.

terminal
npx garn-ui add tabs
First time? Set up garn in your project
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

SelectorState
[data-state=active]active
[data-state=inactive]inactive
:focus-visiblefocus-visible
[disabled]disabled
PropertyTokenTier
listHeight--garn-control-h-mdsemantic · air
ring--garn-ringbrandablesemantic

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

Do
  • Give each TabsTrigger a value and a matching TabsContent.
  • Keep labels short so the tablist fits on one line.
  • Set defaultValue (uncontrolled) or value + onValueChange (controlled).
Don't
  • 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

ArrowLeftArrowRightMove between tabs (horizontal orientation).
ArrowUpArrowDownMove between tabs (vertical orientation).
HomeEndJump to the first / last tab.
TabMove focus from the tablist into the active panel.
EnterSpaceActivate the focused tab (manual activation mode).
A1.3.1Info and RelationshipsA2.1.1KeyboardAA2.4.7Focus VisibleA4.1.2Name, Role, Value
  • 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.