Designer view

Tabs

A switching control that exposes a flat set of mutually exclusive panels through a row (or column) of tab triggers. Used to chunk related content under a single surface so only one chunk is visible at a time.

When to use

Use

When chunking related content into 3–7 mutually exclusive panels under a single surface where the user picks one chunk at a time. Best for parallel views of the same subject (a settings page with "Account / Notifications / Billing" panels) or alternative presentations of the same data.

Avoid

For sequential steps that must complete in order — that is `Stepper` or a wizard pattern. For navigation between top-level pages — that is `SidebarNav` or breadcrumbs. For more than seven panels — split into a navigation hierarchy or a search interface; tab-row overflow is a recovery pattern, not a design target.

Versus related

  • accordion

    `Accordion` allows multiple panels open at once (or one at a time, but visually stacked); `Tabs` always show one panel and replace it on selection. Accordion preserves all panel labels on screen; Tabs surface only one panel's content.

  • segmented-control

    `SegmentedControl` switches between mutually exclusive *views of the same content* (e.g. "List / Grid" toggle); `Tabs` switches between *different content* under the same heading. Segmented controls tend to live above the content they control; tabs replace it.

  • sidebar-nav

    `SidebarNav` navigates between independent pages with their own URLs; `Tabs` swap inline content within one page. Vertical Tabs and SidebarNav can look similar — the distinguisher is whether selecting changes the URL.

Highlight
Fig 1.1 · Tabs · Designer view
Designer

Figma anatomy

Slot Figma type Hint
tablist frame Auto-layout horizontal frame; gap and padding from token set
tab instance Tab item component instance; selected state via component property
indicator rectangle 2-3px line component variant; position bound to selected tab
tabpanel frame Auto-layout vertical frame; visibility driven by selected variant
Designer

Token usage per slot

tablist
spacing
  • paddingspacing.tight
  • gapspacing.tight
radius
  • cornerradius.md
color
  • bordercolor.border.subtle
tab
spacing
  • paddingspacing.compact
radius
  • cornerradius.sm
color
  • foregroundcolor.text.muted
  • ringcolor.border.focus
typography
  • sizetext.sm
  • weightweight.medium
indicator
radius
  • cornerradius.pill
color
  • backgroundcolor.accent.bg
tabpanel
spacing
  • paddingspacing.comfortable
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
  • lineHeightleading.normal
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps line / contained / pill.
OrientationVariantorientationhorizontal / vertical. Vertical is forced to horizontal at and below `breakpoint.sm` per the responsive block.
DensityVariantdensity
ActivationVariantactivationautomatic / manual. In Figma a Variant property since the visual treatment of focus differs slightly between modes; in code it drives whether arrow-key navigation activates on focus.
Tab CountVarianttabs.lengthFigma exposes a Variant for 2 / 3 / 4 / 5 / 6+ tab counts to allow preview-time layout review. Code accepts an array of tab definitions; the count is `.length`, not a separate prop.
SelectedVariantselectedIdFigma exposes the selected tab as a Variant for preview; in code this is controlled state on the parent, not a Figma-style prop.
Designer

Motion

TransitionDuration token
indicatormotion.duration.fast
Easing
motion.easing.standard
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smAt and below, the `vertical` orientation is forced to horizontal with overflow-scroll on the tablist — vertical tab columns claim too much inline space on narrow viewports. Indicator continues to track the selected tab; arrow keys revert to Left/Right.
breakpoint.lgAbove this width, the `orientation` property is honoured as authored. Vertical layouts dock the tablist on the inline-start edge with a fixed inline-size; arrow keys map Up/Down to next/prev tab. The indicator switches from a bottom underline to an inline-start side bar.
Both

Internationalisation

RTL · mirroring

Horizontal tablist renders right-to-left — the first tab in source order appears on the inline-start (visual right in RTL). Arrow-key navigation reverses per APG: ArrowRight moves to the *previous* tab in RTL, ArrowLeft to the *next* — the keys follow visual direction, not source order. Vertical tablists are direction-neutral (Up/Down keys unchanged). Selected-tab indicator slides along the inline axis using logical properties (`inset-inline-start`); slide direction reverses naturally. Overflow scrolling reverses direction too — Home scrolls to the inline-start (visual right in RTL).

Text expansion

Tab labels can grow 30–50% in DE/RU/FI; tablist may overflow the viewport earlier in those languages. Horizontal-scroll handling (per the existing mistake `tabs-overflow-no-scroll-into-view`) accommodates expansion without wrapping. Vertical orientation naturally accommodates expansion via fixed inline-size on the tablist column. Density `compact` is risky for long-text languages — consider `comfortable` as the default in heavy-expansion locales.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

linecontainedpill

Properties

The same component, parameterised.

PropertyType
orientationhorizontal | vertical
activationautomatic | manual
densitycomfortable | compact

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
selectedbusylazyerror
Both

Figma↔Code mismatches

  1. 01
    Figma

    Each tab state (default / hover / selected / disabled) modeled as a separate Figma variant on the tab component

    Code

    A single tab element with `:hover`, `:focus-visible`, and `aria-selected` driving CSS

    Consequence

    Variant explosion: 4 states × 3 sizes × 2 orientations = 24 variants, and developers cannot mechanically map a Figma "hover variant" to a CSS pseudo-class without manual translation.

    Correct

    States belong on a separate states sheet, not as variants. The Figma component captures structural variants (line / contained / pill); the canonical reference documents the state matrix once.

  2. 02
    Figma

    The active-tab underline drawn as a separate decorative line component on the artboard

    Code

    An indicator pseudo-element (or absolute-positioned div) whose position is bound to the selected tab

    Consequence

    Designers move the underline manually when switching the visible tab in mocks; developers wire the indicator to selection programmatically. The two artefacts disagree about which tab is active at any given mock.

    Correct

    Document that the indicator is presentational and *derived from* the selected tab. In Figma, bind the indicator's position to the selected variant; in code, animate the indicator from the selected tab's bounding box.

  3. 03
    Figma

    Vertical tabs drawn as a left sidebar with no semantic relationship to the right-hand content

    Code

    Vertical tabs use the same `role="tablist"` with `aria-orientation="vertical"` and arrow keys reverse from horizontal to vertical

    Consequence

    Designers and developers diverge on whether a vertical layout is "tabs" or a "sidebar nav". The keyboard model and the announced role differ between the two patterns.

    Correct

    Treat vertical tabs as the same component with an `orientation` property. Reserve "sidebar nav" as a distinct pattern with different keyboard semantics (Tab moves between links, no arrow cycling).

  4. 04
    Figma

    Manual vs. automatic activation modeled as two visually identical Figma components

    Code

    A property toggles whether arrow-key navigation activates the tab on focus (automatic) or only on Enter / Space (manual)

    Consequence

    Designers may not realise activation mode is a meaningful decision. Developers default to one mode without knowing the other exists; users with assistive tech feel the difference.

    Correct

    Document `activation` as a first-class property in the canonical reference. Default is automatic for tabs that swap cheap, static content; manual for tabs that load expensive content per panel.

  5. 05
    Figma

    A tablist that overflows the available width drawn either as wrapped onto a second row or simply truncated at the edge

    Code

    Overflowing tablists scroll horizontally with edge-fade affordances and Home / End / arrow keys that scroll the focused tab into view

    Consequence

    Designers either invent a wrapping pattern that breaks the single-row keyboard model, or hide the overflow with no affordance, leaving developers to invent the scroll behavior ad hoc. Keyboard users on overflowing tablists land on tabs that are visually offscreen.

    Correct

    Standardise horizontal scroll for overflowing tablists with edge-fade affordances and explicit "scroll focused tab into view" behaviour on Home / End / arrow navigation. Reserve wrapping for explicit multi-row layouts (rare; usually a sign the structure should be rethought).

Designer

Common mistakes

#tabs-no-arrow-keys

Tab key cycles through every tab instead of arrow keys

Problem

Implementation uses native focusable buttons with no roving `tabindex`, so Tab walks through every tab plus the panels below. Screen-reader users hear "tab 1 of N" announcements conflicting with the actual document order.

Fix

Implement the roving-tabindex pattern: only the selected tab has `tabindex="0"`, all other tabs have `tabindex="-1"`. Arrow keys (Left/Right for horizontal, Up/Down for vertical) move focus between tabs and update the roving tabindex.

#tabs-panel-not-labelled

Tabpanels missing `aria-labelledby` to their tab

Problem

The panel announces only its content with no relationship to the tab that controls it. Screen-reader users navigating the page out of order cannot identify which tab the panel belongs to.

Fix

Each tabpanel sets `aria-labelledby` to the id of its tab. Each tab sets `aria-controls` to the id of its panel.

#tabs-state-as-data-attribute-only

Selection driven only by a `data-selected` attribute, no `aria-selected`

Problem

Visual selection works (CSS targets the data attribute) but assistive tech reports every tab as unselected.

Fix

`aria-selected` is the source of truth. Style from `[aria-selected="true"]` rather than introducing a parallel `data-selected` attribute that has to stay in sync.

#tabs-disabled-but-not-skipped

Disabled tabs cannot receive keyboard focus

Problem

Setting `disabled` on a disabled tab removes it from the focus order entirely, breaking arrow-key navigation between the remaining tabs and stranding users on the disabled tab.

Fix

Use `aria-disabled="true"` instead of the HTML `disabled` attribute. The disabled tab remains focusable and announceable; activation (Enter / Space) is the operation that becomes a no-op.

#tabs-lazy-panel-no-aria-busy

Lazy-loaded tabpanel renders empty without an `aria-busy` announcement

Problem

Selecting a tab fetches its panel content asynchronously. The panel is mounted immediately but empty, so screen-reader users land on a tabpanel with no perceivable content and no signal that anything is loading.

Fix

Set `aria-busy="true"` on the tabpanel while content is in flight, render a visible loading affordance, and announce the transition (e.g. via `aria-live="polite"` on a status region inside the panel). Clear `aria-busy` when content is ready.

#tabs-overflow-no-scroll-into-view

Overflowing tablist does not scroll the focused tab into view

Problem

The tablist scrolls horizontally on overflow, but pressing arrow keys, Home, or End moves focus to a tab that remains offscreen. Sighted keyboard users lose track of focus; screen magnifier users see no movement.

Fix

On focus change inside the tablist (arrow keys, Home, End), call `element.scrollIntoView({ block: 'nearest', inline: 'nearest' })` on the newly focused tab, or compute the scroll offset manually so the focused tab is fully visible with a small inline padding.

#tabs-indicator-not-rtl-aware

Indicator animation hard-codes left-to-right movement

Problem

The selected-tab indicator animates by interpolating `left` / `transform: translateX(...)` from the previous tab to the next. Under `dir="rtl"` the indicator slides the wrong way or ends up offset from the active tab.

Fix

Compute the indicator position from the selected tab's bounding box (`getBoundingClientRect`) relative to the tablist, not from a directional offset. Use logical properties (`inset-inline-start`) or transform values that respect writing direction.

Accessibility hints
Slot Accessibility hint
tablist `role="tablist"` with `aria-orientation` matching the visual orientation. If the tabs label a region elsewhere on the page, give the tablist an `aria-label` or `aria-labelledby`.
tab `role="tab"`. Exactly one tab in the list has `aria-selected="true"` at any time. `aria-controls` references the associated tabpanel id. Disabled tabs use `aria-disabled` and remain focusable so keyboard users can still navigate to them.
indicator Presentational only. Selection is communicated through `aria-selected` on the tab; the indicator is a visual reinforcement, not a substitute.
tabpanel `role="tabpanel"` with `aria-labelledby` pointing at its tab. Make the panel programmatically focusable (`tabindex="0"`) only if the panel itself is the first interactive surface; otherwise let the first interactive child receive focus naturally.