Skip to main content
Graphite UI

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.

Contract 2.0.0Kit · 6 sets · 62 variantsOpen in Figma

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.

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.json
import { 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.

  1. Tab listRequiredMinimum 2 tabs.
  2. Panel content per tabRequired
  3. 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.

Style: Line

Panel content.

Style: Contained
Contained, Grid aware
Line, Large
Type: Icon only
Type: Icon only, Large
Text + Icon
2nd label
Dismissible
Overflow
Orientation: Vertical

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.

Line
Line, Hover
Line, Focus
Contained
Contained, Focus

API reference

Generated from the contract, versioned with it, and checked by drift-check. This table cannot describe props the component does not have.

Tabs props
PropTypeDefaultNotes
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.
iconOnlyboolean—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.
fullWidthboolean—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 tablist with aria-orientation. Each tab is a button with role="tab", aria-selected and aria-controls; each panel is a tabpanel labelled 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 axes and how they map to code
Kit axisValuesCodeHow it maps
StyleLine · ContainedvariantLine: 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.
TypeText + Icon · Icon onlyiconOnly · tab.iconIcon 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.
SizeMedium · Largesize40 and 48. Contained is always Large.
AlignmentAuto-width · Grid awarefullWidthGrid aware shares the width equally.
Item optionsDismissible · Hover Dismissible · 2nd label · Badge indicatoronDismiss · dismissOnHover · secondaryLabel · badgeThe close glyph (and Delete); the 12/16 second line; the 8px danger dot over an icon.
Previous · NextBooleans, 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 tabsSeparate setorientationA 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.
SelectedFalse · TruedefaultTabIdRuntime state. The prop picks which tab starts selected; after that the component owns it.
StateEnabled · Hover · Focus · Selected · Disabledtab.disabledHover and Focus are pseudo-classes (governance rule 7). Disabled is per tab. Skeleton has no counterpart by rule.