Design System

Design System

Tabs

Navigation

Open in Storybook: Tabs (opens in a new tab)

On this page

Overview

Tabs switch between peer views of the same thing, in place, without changing the URL or the page. HTML has no tab element, so ARIA is the semantics: the roles replace the list, and a screen reader hears tabs, not a list.

The rule. A named ul role="tablist" of li role="tab", and a div role="tabpanel" for each, wired by ids. tabindex follows selection, the li gets click, Enter and Space by hand, and focus never moves into the panel by script.

Anatomy

  1. Tablist

    ul role="tablist"

    Holds the tabs and names the set.

  2. Tab

    li role="tab"

    One view of the set. The selected one is in the Tab order.

  3. Tab panel

    div role="tabpanel"

    The view of the selected tab, named by it.

Usage

Use cases

Five common tab sets, each the real component and each working.

  • Product details

    Horizontal · automatic

    Description

    Ristretto with a metal cup support. Intense, short, with notes of cocoa.

    aria-label="Product details"

    Arrows show the panel at once: the panels are already in the page.

  • Account settings

    Vertical · automatic

    Security

    Password, two-step sign-in.

    aria-orientation="vertical"

    ArrowUp / ArrowDown replace Left / Right, at every width: the column stays beside its panel on a phone too.

  • Code sample per framework

    Horizontal · manual

    HTML

    <ul role="tablist">…</ul>

    Enter / Space selects

    Manual when showing a panel is slow (fetch, heavy render). tabindex follows selection, not the arrows.

  • Right-to-left

    dir="rtl"

    الوصف

    لوحة الوصف

    Logical properties

    ArrowLeft goes to the next tab, ArrowRight to the previous.

  • Many tabs

    Overflow · scrollable tablist

    March

    Orders in March.

    A wrapper scrolls · ul is max-content

    Scroll a wrapper around the ul, not the ul. An arrow at the end that has more tabs.

Props

Props
PropDoesDefault
itemsThe tabs: id, label, contentRequired
labelNames the tablistRequired
activationautomatic or manualautomatic
orientationhorizontal or verticalhorizontal
scrollableA strip that does not fit scrollsfalse
selectedId · onSelectedIdChangeThe page keeps the selection—
before · afterWhat goes beside the strip—

Example

import { Tabs } from '@/content/examples/tabs/Tabs'

<Tabs
  label="Product details"
  scrollable
  items={[
    { id: 'description', label: 'Description', content: <p>…</p> },
    { id: 'specs', label: 'Specs', content: <p>…</p> },
    { id: 'reviews', label: 'Reviews', content: <p>…</p> },
  ]}
/>

Specs

States

The look is read from aria-selected, never a class, so it cannot drift from what is announced.

States
StateLook
SelectedBold label and a 3px bar
UnselectedRegular label, no bar. Text still 4.5:1
HoverDarker label and a light bar, only under @media (hover: hover)
FocusA 3px ring on :focus-visible, on a selected tab or on one an arrow reached (manual)
Forced coloursThe bar is Highlight and the label CanvasText: bold still shows the state
Reduced motionNo transition on the colour and the bar

Layout

Tabs never wrap onto a second row: it breaks the order of the arrows that people see.

Layout
TablistWhenLayout
One rowThe labels fitEvery tab visible, the bar under the selected one
Scrollable rowThe labels do not fitA wrapper scrolls, with an arrow at each end that has more tabs
VerticalMany tabs or long labels, at any widthOne column beside the panel, which narrows and wraps. Never a row

Markup

<ul role="tablist" aria-label="Product details">
  <li role="tab" id="t1" aria-selected="true"
      aria-controls="p1" tabindex="0">Description</li>
  <li role="tab" id="t2" aria-selected="false"
      aria-controls="p2" tabindex="-1">Specifications</li>
  <li role="tab" id="t3" aria-selected="false"
      aria-controls="p3" tabindex="-1">Reviews</li>
</ul>
<div role="tabpanel" id="p1" aria-labelledby="t1" tabindex="0">…</div>
<div role="tabpanel" id="p2" aria-labelledby="t2" tabindex="0" hidden>…</div>
<div role="tabpanel" id="p3" aria-labelledby="t3" tabindex="0" hidden>…</div>

Overflow

A wrapper scrolls, never the ul: a global ul { overflow: visible !important } would beat it.

<div class="tabs-viewport">             <!-- the arrows sit against this -->
  <div class="tabs-scroll">              <!-- the wrapper scrolls -->
    <ul role="tablist" aria-label="Product details">…</ul>
  </div>
  <button class="tabs-arrow" data-edge="end" type="button"
          tabindex="-1" aria-hidden="true">›</button>   <!-- only if more tabs -->
</div>
.tabs-viewport { position: relative; }
.tabs-scroll {
  overflow-x: auto; scrollbar-width: thin;
  scroll-padding-inline: 2.5rem;       /* a focused tab stays clear of the arrows */
}
ul[role="tablist"] {
  display: flex;
  width: max-content;                  /* the strip keeps its length */
  min-width: 100%;                     /* and fills when it is short */
  list-style: none; margin: 0; padding: 0;
  border-block-end: 1px solid var(--line);
}
[role="tab"] { flex: none; padding: 0.75rem 1rem; white-space: nowrap; }
.tabs-arrow {
  position: absolute; inset-block: 0; margin: auto 0;
  inline-size: 2rem; block-size: 2rem;
}
.tabs-arrow[data-edge="end"] { inset-inline-end: 0; }

Tokens

Component tokens

Component tokens
PartPropertyTokenValue
Tab, unselectedText colourink-soft#4b4b55
Tab, selected and hoverText colourink#16161a
Bar, selectedColourlilac#5e548e
Tablist line, bar on hoverColourline#d7d7dc
Focus ringColour—#1b4fd1
Scroll arrowBorder · fillline · —#d7d7dc · #ffffff

Guidelines

Controls by scenario and context

Each row is a use case, drawn with the real component at half size.

  • PanelOutside the tablist: the tabpanel it controls, in the Tab order

Zoom 400% · 320 CSS px. At 200% text size a horizontal strip scrolls and a vertical column wraps its labels: nothing is truncated.

  • Product details

    Horizontal · automatic

    Live example

    Desktop · 640 px and upFull width

    Tablist · 3 tabs · panel below

    Panel

    aria-label="Product details". Arrows show the panel at once: the panels are already in the page.

    Smaller viewport · 400–639 pxNarrow column or card

    Tablist · 3 tabs

    Panel

    Same; labels never truncated.

    Zoom 400% · 320 CSS pxReflow

    Tablist · 3 tabs

    Short labels fit. A strip that does not fit scrolls in its wrapper, never onto a second row.

  • Account settings

    Vertical · automatic

    Live example

    Desktop · 640 px and upFull width

    Vertical tablist beside the panel

    Panel

    aria-orientation="vertical": ArrowUp / ArrowDown.

    Smaller viewport · 400–639 pxNarrow column or card

    Column beside the panel

    The column stays beside its panel at every width.

    Zoom 400% · 320 CSS pxReflow

    Column; labels wrap

    Labels wrap inside the column; keys unchanged.

  • Code per framework

    Horizontal · manual

    Live example

    Desktop · 640 px and upFull width

    Tablist · 3 tabs · panel below

    Panel

    Manual: arrows move, Enter / Space selects. tabindex follows selection, not the arrows.

    Smaller viewport · 400–639 pxNarrow column or card

    Tablist · 3 tabs

    Same.

    Zoom 400% · 320 CSS pxReflow

    Tablist · 3 tabs

    Short labels fit.

  • Many tabs

    Scrollable tablist

    Live example

    Desktop · 640 px and upFull width

    Tabs · more if room

    A wrapper scrolls, not the ul. An arrow at the end that has more tabs.

    Smaller viewport · 400–639 pxNarrow column or card

    Selected tab in view · the rest scroll

    The selected tab is scrolled into view.

    Zoom 400% · 320 CSS pxReflow

    Selected tab in view · the rest scroll

    Many tabs at 320 px: consider a vertical tablist or a select.

  • Right-to-left

    dir="rtl"

    Live example

    Desktop · 640 px and upFull width

    Tablist mirrored

    Logical properties. ArrowLeft goes to the next tab, ArrowRight to the previous.

    Smaller viewport · 400–639 pxNarrow column or card

    Tablist mirrored

    Same.

    Zoom 400% · 320 CSS pxReflow

    Tablist mirrored

    Same.

Accessibility

Keyboard

Keyboard
KeyAutomatic activation (default)Manual activation
ArrowRight · ArrowLeftNext or previous tab, wrapping: focus and selectFocus only
Home · EndFirst or last tab: focus and selectFocus only
Enter · SpaceSelect the focused tab. preventDefault on Space or the page scrollsSame
VerticalArrowUp / ArrowDown replace Left / RightSame
Right-to-leftArrowLeft goes to the next tab, ArrowRight to the previousSame

Focus

Focus
MomentWhere focus goes
Tab into the tablistThe selected tab, the only one with tabindex="0"
Tab out and back, manualThe selected tab, not the one an arrow reached
Tab from the selected tabThe panel (tabindex="0"), then its own controls. Shift + Tab goes back
A tab is activatedFocus stays on the tab
Tabs are addedNew ids from a counter that only climbs, so none repeats
An overflowing tablistThe strip scrolls so the selected tab is in view, clear of the arrows. Only the strip moves

Roles and properties

The accessibility tree of Chromium and WebKit. No screen reader was run: listen in NVDA, JAWS and VoiceOver.

Roles and properties
PartExposed
Tablisttablist, named by the label, with its orientation
Tabtab, “selected”, “1 of 3”, named by its visible text
Tab paneltabpanel, named by its tab
List, scroll arrowNone: no list or listitem is left, and the arrow is aria-hidden

Support

Support
BrowserChecked
ChromiumYes
WebKit (Safari)Yes
FirefoxNo

WCAG criteria

WCAG criteria
CriterionMet by
1.3.1 Info and Relationships (A)tablist, tab and tabpanel, wired by aria-controls and aria-labelledby
1.3.2 Meaningful Sequence (A)Tabs in DOM order, the panel right after the tablist
1.4.1 Use of Color (A)A bold label and a bar, not colour alone
1.4.3 Contrast (Minimum) (AA)Unselected 8.62:1, selected 18.04:1
1.4.10 Reflow (AA)Only the wrapper around the tablist scrolls at 320 px
1.4.11 Non-text Contrast (AA)The bar 6.70:1 and the focus ring 6.81:1
2.1.1 Keyboard (A)Arrows, Home, End, Enter and Space, Tab and Shift + Tab
2.4.3 Focus Order (A)A roving tabindex that follows selection; Tab to the panel
2.4.7 Focus Visible (AA)A 3px ring on :focus-visible
2.4.11 Focus Not Obscured (Minimum) (AA)The focused tab stays in view, clear of the arrows
2.5.3 Label in Name (A)The visible text of the tab is its name
2.5.8 Target Size (Minimum) (AA)Tabs over 24 × 24 CSS px, arrows 32 × 32
3.2.1 On Focus (A)Showing a panel changes content, not context
4.1.2 Name, Role, Value (A)aria-selected kept live, no leftover list

Related

This section is still in progress.

References

This section is still in progress.