Skip to main content

Core · Navigation

Tabs

Switches between panels of related content in one place; without JavaScript the panels are stacked sections.

Example

Three tabs, Summary, Settings and Logs, each with its panel.

Three services are running.

Deploys run on every push.

No errors in the last hour.

Focus a tab, then use the arrow keys, Home and End. The Code and Design tabs of this page are the same component.

Known issues

None known.

Code

Copy the markup

HTML
<nav class="luz-tabs" aria-label="Project">
  <a class="luz-tab" href="#project-summary">Summary</a>
  <a class="luz-tab" href="#project-settings">Settings</a>
</nav>
<div class="luz-tabpanel" id="project-summary">…</div>
<div class="luz-tabpanel" id="project-settings">…</div>
HTML
<nav class="luz-tabs luz-tabs--pill" aria-label="Project">
  <a class="luz-tab" href="#project-summary">
    <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">…</svg>Summary
  </a>
  <a class="luz-tab" href="#project-settings">
    <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">…</svg>Settings
  </a>
</nav>
JS
import { initTabs } from "@luz-ds/core/tabs";
initTabs();

Variants and options

One class changes the look:

ClassEffect
.luz-tabs
A row of text tabs over a rule; the open one is brand text with a brand underline
.luz-tabs--pill
Pill: a tab may hold an icon (an svg before the text, at the size and colour of the text), and the open tab is a chip on --luz-surface with no rule under the row. Below 640 px the icons are hidden, so the tabs fit

The tab that opens first is the first one that matches:

OrderTab
1
The tab whose panel holds the element named in the URL fragment
2
A tab with aria-selected="true" or .is-active in the markup
3
The first tab

How it works

A row of in-page links to the panels, styled by components/tabs.css and upgraded by tabs.js.

  • initTabs() adds the ARIA tabs pattern: role="tablist", "tab" and "tabpanel", aria-selected, aria-controls, aria-labelledby and a roving tabindex. It hides the inactive panels. luz.js runs it.
  • A link to an element inside a hidden panel opens its tab, on load, on hashchange and on click.
  • Inactive panels are hidden="until-found" where the browser supports it, so find in page reaches them and opens their tab.
  • activateTab(tab) selects a tab from your own script.

Without JavaScript

The tabs are a row of links, and every panel is shown, stacked, in order. Each link jumps to its panel.

Give each panel a heading, so the stacked page still reads well.

In the pill look the chip is drawn for aria-selected="true", which the script sets: without JavaScript no tab has a chip.

Keyboard and ARIA

Tab moves into the tab row, to the selected tab; Tab again leaves for the panel.

  • Left and Right (or Up and Down) move to the next tab and select it; Home and End go to the first and the last.
  • Space or Enter selects the focused tab.
  • In right-to-left text, Left and Right follow the reading direction.
  • Name the tab row with aria-label; the name stays when it becomes a tab list.

Contrast and focus are in the Design tab.

Design

When to use

Use tabs to switch between panels of the same level that readers do not need side by side, such as the summary, settings and logs of one service.

When not to use

Do not use tabs to move between pages, or to hide content readers need all at once.

  • Between pages, use a sidebar or header links.
  • For steps in order, use separate pages.
  • More than five or six tabs do not fit a phone: use sections with headings.

Do and don't

Do Tabs named after their panels, in a word or two.
Don't Numbered steps as tabs: a process in order belongs on separate pages, and nothing stops the reader from skipping a step.

Writing the content

Name each tab after its panel, in one or two words.

  • Sentence case, no verbs: "Summary", "Settings".
  • Use the same name for the panel's heading.

Contrast and focus

Tabs are secondary text; the current one is brand text over a brand underline. In the pill look it is also on a chip.

PartColour on backgroundLightDarkHigh contrastNeeds
Tab
--luz-text-secondary on --luz-surface-page
6.2:1
7.5:1
19.3:1
4.5:1
Tab, hover
--luz-text-primary on --luz-surface-page
15.8:1
15.1:1
21.0:1
4.5:1
Current tab and its underline
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
4.5:1
Current tab, pill
--luz-brand-primary on --luz-surface
5.9:1
7.2:1
14.1:1
4.5:1
Focus ring
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: a 2 px --luz-focus-ring outline drawn inside the tab, so the scrolling row never cuts it.
  • Not colour alone: the current tab has a 2 px underline; the row's line under the others is --luz-divider. The pill look keeps the underline and the weight on the chip.
  • Icons: mark an icon aria-hidden="true", so the tab is named by its text; below 640 px the icon is hidden, so never let it carry the meaning alone.

Tokens it uses

The 15 tokens that components/tabs.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-2xs, --luz-space-md, --luz-space-sm, --luz-space-xs
Typography
--luz-font-sans, --luz-text-md, --luz-text-sm
Radius
--luz-radius-md
Motion
--luz-duration-fast

Keyboard and ARIA are in the Code tab.