Tabs
A row of labels that switches between peer views of the same content, one visible at a time. Use it when the views are alternatives, never for steps a reader has to take in order.
Live preview
Rendered by the component itself from the same generated tokens as the rest of the site.
One hex value. Everything else on the page is derived from it.
Tonal ramps sampled from the source, one for each colour family.
The named colours components bind to, checked for contrast at AA or AAA.
<Tabs
tabs={[
{ id: 'source', label: 'Source', panel: <p>One hex value. Everything else on the page is derived from it.</p> },
{ id: 'ramps', label: 'Ramps', panel: <p>Tonal ramps sampled from the source, one for each colour family.</p> },
{ id: 'roles', label: 'Roles', panel: <p>The named colours components bind to, checked for contrast at AA or AAA.</p> },
]}
/>Installation
Install it with the shadcn CLI, along with everything it uses. New project? Set it up first, as Quick start shows.
npx shadcn@latest add https://www.graphite-ui.com/r/tabs.jsonimport { Tabs } from '@/components/ui/tabs'
import type { Tab } from '@/components/ui/tabs'Anatomy
Two slots: the tab list and one panel per tab. The type of the tabs array is a tuple of at least two, so a single tab is a compile error rather than a review comment.
Panel content.
Panel content.
Panel content.
- Tab listRequiredMinimum 2 tabs.
- Panel content per tabRequired
- Previous and NextOptionalThe kit's overflow buttons. Shown only while the list overflows, each disabled at its end; the list scrolls under them rather than wrapping.
Variants
Line and Contained are the kit’s two styles; Icon only, Grid aware, the trailing icon, the 2nd label, the badge and dismissible tabs are its item options. The kit draws vertical tabs as a separate set; the code makes them a value of one component, so a layout change is not a component swap. When the list is wider than its container it scrolls under the kit’s Previous and Next buttons.
Panel content.
Panel content.
Panel content.
Panel content.
Panel content.
Panel content.
States
The selected tab changes tone, takes SemiBold and carries the indicator, so its state never rests on colour alone. Hover lifts a label to on-surface. Focus is a ring inside the tab that replaces its rule. A disabled tab dims and is stepped over by the arrow keys.
API reference
Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.
| Prop | Type | Default | Notes |
|---|---|---|---|
| orientation | 'horizontal' | 'vertical' | — | |
| variant | 'line' | 'contained' | — | The kit's Style. Horizontal only; vertical tabs are their own set in the kit and their own look in the code. |
| size | 'md' | 'lg' | — | The kit's item Size, 40 and 48. Contained is always 48. |
| iconOnly | boolean | — | The kit's Type=Icon only. Every tab needs an icon, and its label becomes the tab's aria-label, so a tab is never unnamed. |
| fullWidth | boolean | — | The kit's Alignment=Grid aware. The tabs share the width equally. |
| onDismiss | — | — | The kit's Dismissible. Each tab carries the close glyph, and Delete on a focused tab dismisses it; the caller removes the tab from the list. dismissOnHover is the kit's Hover Dismissible. |
| tabs | — | — | Each tab takes id, label and panel, and the kit's item options, icon (trailing, or the whole of an icon-only tab), disabled, secondaryLabel (the kit's 2nd label, Contained) and badge (the kit's Badge indicator, icon only). |
Design tokens
Every swatch is live. Change the source color in the header and this table repaints, because it reads the same roles the component does.
primaryThe selected tab's indicator (the bottom rule on Line, the top rule on Contained, the left bar on vertical); the focus ring, through the family's focus step; and disabled tabs, through its disabled and disabled-content steps.on-surfaceThe selected and hovered label, and the overflow buttons' chevrons.on-surface-variantLabels at rest and the 2nd label, the lower tone-step of on-surface.outlineEach Line tab's own 2px rule at rest, Contained's dividers, and the vertical items' rules and resting indicator.surfaceThe selected Contained tab, and vertical items at rest.surface-variantContained tabs at rest, Contained's overflow buttons, vertical hover, and the close glyph's hover square.backgroundLine's overflow buttons and the 16px fade they draw over the list.dangerThe badge dot.textLabels at Body/3, SemiBold when selected; the 2nd label at 12/16.spacingTab heights and padding, list gaps, and the badge inset.
Usage
The contract's prohibitions, written as the choices you will actually face.
Do
- Use tabs for peer views of one thing, like Preview and Code on this page, where a reader might want either first.
- Put a form inside a panel if it belongs there. Every panel stays mounted, so switching away and back keeps what was typed.
- Keep labels to a word or two, so the list fits its container. It scrolls under Previous and Next when it does not, but a list that fits needs no hunting.
- Switch to vertical when the list outgrows the width, or when the tabs sit beside a panel that is taller than it is wide.
Don’t
- Use tabs for a sequence. If step two depends on step one, a reader should not be able to jump straight to it.
- Ship a single tab. With nothing to switch to, it is a heading pretending to be a control.
- Restyle the indicator away and leave the selected tab marked by colour alone.
- Key a panel on the active tab or render it conditionally yourself. That remounts the panel and throws away any form state inside it.
Accessibility
What the component does for you, and what it leaves to you.
- Keyboard
- Only the selected tab is in the Tab order. Left and Right (Up and Down when vertical) select the previous or next tab and wrap at the ends. Home and End select the first and last. Selection follows focus, so the panel changes as soon as a tab is reached.
- Roles
- The list is a
tablistwitharia-orientation. Each tab is a button withrole="tab",aria-selectedandaria-controls; each panel is atabpanellabelled by its tab. - Focus
- The focus ring is
--graphite-primary-focus, inset so it stays inside the tab. The arrow keys move focus and the selection together, skipping disabled tabs, so the ring is always on the selected tab. - Icon only
- The label becomes the tab’s
aria-label, so an icon-only tab is never unnamed. - Dismissing
- Delete on a focused tab dismisses it, announced through
aria-keyshortcuts; the close glyph is for the pointer. The overflow buttons are out of the Tab order: the arrow keys scroll the selected tab into view. - Contrast
- The selected label is on-surface and SemiBold, and the rest are on-surface-variant, one tone step lower. Both are measured against surface at the theme’s target.
- Panels
- Inactive panels are hidden with the hidden attribute, not unmounted, so assistive tech skips them and their state survives.
- Motion
- Nothing animates. The indicator moves instantly, so there is nothing for reduced motion to switch off.
Figma parity
The kit's Tabs page ships 62 variants across 6 sets, most of them the private tab items the two public sets are built from. The code covers every axis of the public set and of the items it is built from, as props on Tabs and options on each tab.
| Kit axis | Values | Code | How it maps |
|---|---|---|---|
| Style | Line · Contained | variant | Line: each tab its own 2px rule, a pixel apart. Contained: 48 tall, surface-variant with a divider, the selected tab on surface with a 2px primary rule on top. The kit’s Contained Auto-width items are 50 tall, a slip; built at 48. |
| Type | Text + Icon · Icon only | iconOnly · tab.icon | Icon only squares the tab (40 or 48) and makes the label its aria-label. A text tab with an icon carries it 8 after the label. |
| Size | Medium · Large | size | 40 and 48. Contained is always Large. |
| Alignment | Auto-width · Grid aware | fullWidth | Grid aware shares the width equally. |
| Item options | Dismissible · Hover Dismissible · 2nd label · Badge indicator | onDismiss · dismissOnHover · secondaryLabel · badge | The close glyph (and Delete); the 12/16 second line; the 8px danger dot over an icon. |
| Previous · Next | Booleans, and the Tabs button item | — | Shown when the list overflows. The kit’s glyph is a plus placeholder and Line’s fade a raw white; built as a chevron and from background. |
| Vertical tabs | Separate set | orientation | A set in the kit, a value in the code. Kept as a prop because the capability exists in the kit and removing a prop is breaking. |
| Selected | False · True | defaultTabId | Runtime state. The prop picks which tab starts selected; after that the component owns it. |
| State | Enabled · Hover · Focus · Selected · Disabled | tab.disabled | Hover and Focus are pseudo-classes (governance rule 7). Disabled is per tab. Skeleton has no counterpart by rule. |
