Tabs

.tabs[data-ui="tabs"]

Tabs — one strip of labels that switches panels in place.

Examples

1 — tabs, with the behaviour

View “1 — tabs, with the behaviour” as

Your name, avatar and public handle.

<div class="tabs" data-ui="tabs">
  <div class="tablist" role="tablist" aria-label="Account settings">
    <button class="tab" type="button" role="tab" id="tab-profile"
            aria-controls="panel-profile" aria-selected="true">Profile</button>
    <button class="tab" type="button" role="tab" id="tab-billing"
            aria-controls="panel-billing" aria-selected="false">Billing</button>
    <button class="tab" type="button" role="tab" id="tab-alerts"
            aria-controls="panel-alerts" aria-selected="false">Notifications</button>
  </div>

  <div class="tabpanel" role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
    <p>Your name, avatar and public handle.</p>
  </div>
  <div class="tabpanel" role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" hidden>
    <p>Card on file, invoices and the plan you are on.</p>
  </div>
  <div class="tabpanel" role="tabpanel" id="panel-alerts" aria-labelledby="tab-alerts" hidden>
    <p>Which events send you an email.</p>
  </div>
</div>

The roles and data-ui="tabs" are one indivisible thing. role="tablist" tells a keyboard user the arrow keys move between tabs — a promise only the script keeps, so copying the roles without it produces a widget that announces a route and dead-ends.

2 — in-page links, without it

View “2 — in-page links, without it” as

Profile

Your name, avatar and public handle.

Billing

Card on file, invoices and the plan you are on.

<div class="tabs">
  <nav class="tablist" aria-label="Account settings">
    <a class="tab" href="#profile">Profile</a>
    <a class="tab" href="#billing">Billing</a>
    <a class="tab" href="#alerts">Notifications</a>
  </nav>

  <section class="tabpanel" id="profile" aria-labelledby="h-profile">
    <h3 id="h-profile">Profile</h3>
    <p>Your name, avatar and public handle.</p>
  </section>
  <section class="tabpanel" id="billing" aria-labelledby="h-billing">
    <h3 id="h-billing">Billing</h3>
    <p>Card on file, invoices and the plan you are on.</p>
  </section>
</div>

Not a degraded version of the first — a correct component. No roles, no script, nothing claimed: Tab walks the links, Enter jumps, find-in-page finds every word.

If you are not shipping the JavaScript, this is the one you want.

Tokens 20

Level 2, declared on .tabs itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by tabs
TokenDefault
--tab-bgtransparent
--tab-colorvar(--color-on-surface-muted)
--tab-font-sizevar(--font-size-sm)
--tab-font-weightvar(--font-weight-medium)
--tab-gapvar(--space-2)
--tab-hover-bgtransparent
--tab-hover-colorvar(--color-on-surface)
--tab-indicator-colorvar(--color-primary)
--tab-indicator-width2px
--tab-min-size2.75rem
--tab-padding-blockvar(--space-3)
--tab-padding-inlinevar(--space-4)
--tab-radiusvar(--radius-field)
--tab-selected-bgtransparent
--tab-selected-colorvar(--color-primary)
--tablist-gapvar(--space-1)
--tablist-rail-colorvar(--color-border)
--tablist-rail-widthvar(--border-width)
--tabpanel-padding-blockvar(--space-1)
--tabs-gapvar(--space-4)

Variants and states

Variants

None. It is one shape, and the page does the rest.

Inside it

  • .tab
  • .tabs-vertical
  • .tablist
  • .tabpanel
  • .tabs-pill
  • .tabs-even

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :disabled
  • :focus
  • :hover
  • data-legacy

Before you ship it

This one needs a script. Opt the markup in with data-ui="tabs" and import import 'mostlycss/js/components/tabs' — that specifier and not the package root, which is a bundle carrying a second copy of the registry, so a page holding both initialises every element twice. What you lose without the script is listed below — for some components that is only polish, for others it is the whole interaction.

What you have to do 1 requirements

  1. Pick one of the two markups above and copy all of it. Then: Name the strip. aria-label on the role="tablist", or on the <nav>. A page with two of these and no names announces "tab list" twice. In markup 1, wire the pairs. Every tab needs aria-controls pointing at its panel's id and every panel aria-labelledby pointing back at its tab. tabs.ts logs an error for a tab whose aria-controls resolves to nothing, because selecting it would switch nothing. Do not put a heading inside a role="tabpanel" that repeats its tab. The panel is already labelled by the tab. Markup 2 is the opposite: its sections carry the headings, because there are no tabs to borrow a name from.

src/css/components/tabs.css · npx mostlycss add tabs