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
<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>
<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>
import { initTabs } from "@luz-ds/core/tabs";
initTabs();
Variants and options
One class changes the look:
| Class | Effect |
|---|---|
.luz-tabs | A row of text tabs over a rule; the open one is brand text with a brand underline |
.luz-tabs | 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:
| Order | Tab |
|---|---|
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-labelledbyand a rovingtabindex. It hides the inactive panels.luz.jsruns it.- A link to an element inside a hidden panel opens its tab, on load, on
hashchangeand 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
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.
| Part | Colour on background | Light | Dark | High contrast | Needs |
|---|---|---|---|---|---|
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-ringoutline 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.
- Color
--luz-brand-primary,--luz-divider,--luz-focus-ring,--luz-surface,--luz-text-primary,--luz-text-secondary- 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.