skip to content
menu
components · tabs

tabs

Tabs let people switch between views of the same thing, such as the overview, the logs and the metrics of one service. The selected tab is underlined in blue.

level=info msg="rollout complete" pod=api-7f9c node=worker-2

when to use

  • To switch between views of one thing that sit at the same level: the “overview”, “logs”, “metrics” and “events” of one service.
  • When each view makes sense on its own, and people move between them in no fixed order.
  • For two to six short labels that fit on one row.

when to use something else

  • To filter one list. Use filter tabs: each tab shows a count over its label, and the list below follows the choice.
  • To go to another page or app. Use links. For a home page of apps, use tiles in a tile group.
  • For more views than fit on one row. Use a select. Today the row of tabs doesn't scroll, wrap or move the extra tabs into a menu.
  • To turn a setting on or off. Use a switch.

anatomy

  1. Tab list. The row of tabs, with a thin line underneath.
  2. Tab. A label in monospace and lowercase, in grey, with room around it.
  3. Selection. The selected tab's label turns blue, and a blue underline lies over the row's line, under that tab.
  4. Panel. The view itself. It isn't part of the component: it goes right below the row, and what it needs is under accessibility.

variants and states

state looks
At rest A grey label
Under the pointer The label brightens
Selected A blue label, with a blue underline
Focused An outline inside the tab, when the keyboard brings focus to it
Disabled Faded. It can't be focused or clicked, and the arrow keys pass over it

behaviour

  • A click selects a tab.
  • The row is one tab stop, on the selected tab. The left and right arrow keys move along the row and select as they go, wrapping around at the ends. home and end jump to the first and the last tab that isn't disabled.
  • Selection follows the arrow keys: every press switches the view. Keep the views quick to switch, or load a slow one only once the selection has settled.

The full keyboard map is on the tabs' React page.

accessibility

  • A screen reader announces a row of tabs, each tab, and which one is selected.
  • Name the panel. The tabs don't include the panel, so naming it is up to you: name it after the selected tab. When it holds nothing that can take focus, make it a tab stop, so people who use a keyboard can reach its content. The example at the top of this page does this.

Today: a disabled tab can't take focus, so people who use a screen reader don't learn that it exists.

content

  • One or two words per tab, and nouns: “overview”, “logs”, “metrics”. No verbs and no counts; a count belongs in filter tabs.
  • The same views in the same order on every object of a kind, so “logs” is always in the same place.
  • Write the labels as they should be read. The lowercase look is only a style.

do and don't

do
Views of one thing, at one level.
don't
Counts that filter one list are filter tabs, which show the count and connect the tabs to the list.
do
One or two words, and nouns.
don't
Sentences and verbs make the row long and slow to scan.
do
Leave out a view that doesn't apply.
don't
A disabled tab can't be focused, and doesn't say why it is off.
  • Filter tabs: tabs that filter one list, with counts, a named row of tabs and the list connected to it.
  • Select: when the views don't fit on one row.
  • Tile group: for moving between apps rather than views.

specs

state spec
At rest --fg-muted label
Under the pointer --fg label
Selected --cloud-blue label and a 2px --cloud-blue underline. Set with value (and onChange), or defaultValue
Focused A 2px --focus-ring outline inside the tab (outline-offset: -2px), on :focus-visible
Disabled disabled: true on an item. 40% opacity, a not-allowed cursor; a disabled button, so it is out of the tab order, and the arrow keys skip it
  • Tab list: a row with a 1px --border rule underneath, role="tablist". It doesn't scroll, wrap or overflow.
  • Tab: a button with role="tab" and aria-selected: JetBrains Mono 500, 13px, padding 8px 12px, --fg-muted, lowercased with text-transform. Only the selected tab has tabindex="0".
  • Underline: a 2px bottom border, pulled 1px down, so it lies over the list's rule.
  • Items: items, each with a value, a label and an optional disabled. Without value or defaultValue, the first item is selected; onChange receives the new value.
  • Panel: not rendered. Give yours role="tabpanel", an aria-label after the selected tab, and tabIndex={0} when it holds nothing focusable, as the first example does.
  • Motion: the label and underline colours change in 120ms ease.