Tabs
A row of tabs over their panels, showing one at a time when its module is loaded and all of them when it is not.
Example
View Code
<div class="tabs">
<div role="tablist" aria-label="Account">
<button type="button" role="tab" id="tab-profile" aria-controls="panel-profile">Profile</button>
<button type="button" role="tab" id="tab-billing" aria-controls="panel-billing">Billing</button>
</div>
<section role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
<p>Your name and how people reach you.</p>
</section>
<section role="tabpanel" id="panel-billing" aria-labelledby="tab-billing">
<p>Your plan and your invoices.</p>
</section>
</div>
When to use it
Alternative views of one thing: a profile and its billing, a chart and its table. Not for steps in a sequence, which want a form, and not for hiding content a reader needs to compare against something else.
How it works
A tab list of buttons over their panels. The CSS styles the list and marks the selected tab with the hue on its edge, and it never hides a panel. That is deliberate: a page that does not load tabs.js, or where the script fails, shows every panel under its own tab, so nothing a reader came for is locked away.
Load the module and it pairs every tab with the panel its aria-controls names, shows one, hides the rest, and takes over the keyboard: arrows move selection, Home and End jump to the ends, and only the selected tab is in the tab order, so Tab leaves the list rather than walking it. A link into a hidden panel opens its tab: on load and on every hashchange the module looks for the URL's fragment inside a panel and, if it finds one, selects that panel's tab and brings the target into view, without moving keyboard focus to the tab. Tabs may nest inside another tab's panel: each set keeps its own selection and its own arrow keys, and a link into the inner set opens every enclosing panel on the way. data-emphasis="high" fills the selected tab with the variant instead of underlining it, its top corners (or, in a vertical list, the corners away from the divider) rounded like any other filled control.
<div class="tabs" data-emphasis="high">
<div role="tablist" aria-label="Report">
<button type="button" role="tab" id="t-summary" aria-controls="p-summary" aria-selected="true">Summary</button>
<button type="button" role="tab" id="t-detail" aria-controls="p-detail">Detail</button>
</div>
<section role="tabpanel" id="p-summary" aria-labelledby="t-summary"><p>Summary panel.</p></section>
<section role="tabpanel" id="p-detail" aria-labelledby="t-detail" hidden><p>Detail panel.</p></section>
</div>
<div class="tabs" data-orientation="vertical">
<div role="tablist" aria-label="Settings">
<button type="button" role="tab" id="t-general" aria-controls="p-general">General</button>
<button type="button" role="tab" id="t-keys" aria-controls="p-keys">API keys</button>
</div>
<section role="tabpanel" id="p-general" aria-labelledby="t-general"><p>General settings.</p></section>
<section role="tabpanel" id="p-keys" aria-labelledby="t-keys"><p>Your API keys.</p></section>
</div>
Accessibility
Name the tablist, since a page may have more than one. Each tab's aria-controls names its panel and each panel's aria-labelledby names its tab; the module reads that pairing rather than guessing from document order, so the markup stays the source of truth. The module also maintains aria-selected and the roving tabindex, which is what makes a tab list feel like one control rather than a row of buttons. Without the module the markup is still valid: the roles are right and every panel is readable.
Attributes
| Attribute | Type | Values | Default | Description |
|---|---|---|---|---|
data-variant |
enum | primary, secondary, success, warning, alert, danger, neutral, black, white |
primary |
The hue of the selected tab and its edge. |
data-orientation |
enum | horizontal, vertical |
horizontal |
Whether the tab list runs along the top or down the side. |
data-gap |
enum | none, xs, sm, md, lg, xl, 2xl, 3xl, xs-sm, xs-md, xs-lg, xs-xl, xs-2xl, xs-3xl, sm-md, sm-lg, sm-xl, sm-2xl, sm-3xl, md-lg, md-xl, md-2xl, md-3xl, lg-xl, lg-2xl, lg-3xl, xl-2xl, xl-3xl, 2xl-3xl |
md |
Space between the list and the panels. |
data-emphasis |
enum | high |
The selected tab is filled with the variant; without it the selected tab is underlined. |
Children
> [role="tablist"]: exactly 1. The tabs, in order, with a name of its own.[role="tab"]: at least 2. A button whose aria-controls names its panel.> [role="tabpanel"]: at least 2. A panel whose aria-labelledby names its tab.
Tokens
| Token | Description |
|---|---|
--yeti-tabs-border |
The line under or beside the list. |
--yeti-tabs-padding |
Padding of one tab. |
--yeti-border-width |
Width of that line; the selected mark is twice it. |
--yeti-space-md |
The default gap between the list and the panel. |
--yeti-color-primary |
The default variant's color, when data-variant is absent. |
--yeti-color-primary-subtle |
The default variant's tint. |
--yeti-color-primary-soft |
The default variant's soft stop. |
--yeti-color-primary-strong |
The default variant's strong stop. |
--yeti-color-primary-text |
The default variant's text color. |
--yeti-on-primary |
Text on the default variant's color. |
--yeti-space-xs |
Gap between tabs. |
--yeti-weight-strong |
Weight of a tab's label. |
--yeti-duration-fast |
How long a tab takes to change color. |
--yeti-ease |
The curve of that transition. |
--yeti-radius-sm |
Corner radius of the selected tab with data-emphasis="high". |
Internal tokens (may change between minor versions)
- `--_yeti-variant` - `--_yeti-variant-subtle` - `--_yeti-variant-text` - `--_yeti-gap` - `--_yeti-variant-soft` - `--_yeti-variant-strong` - `--_yeti-on-variant`Accessibility
- Give the tablist a name with aria-label or aria-labelledby. Every tab is a button whose aria-controls names its panel, and every panel names its tab back with aria-labelledby; that pairing is what the module reads. With tabs.js loaded one tab is selected, the rest take tabindex="-1" for a roving focus, and the inactive panels are hidden. Without it nothing is hidden and every tab is focusable, which is plainer but never traps content.
| Key | Action |
|---|---|
Left / Right |
Moves selection along a horizontal list. |
Up / Down |
Moves selection along a vertical list. |
Home / End |
Selects the first or the last tab. |
Tab |
Leaves the list for the selected panel. |
Browser support
- Used without guards: logical properties
- Behind
@supports: nothing
JavaScript
Optional enhancement: components/tabs/tabs.js. The component works without it.
Each event bubbles, crosses a shadow boundary, and cannot be cancelled.
| Event | Module | Detail | Description |
|---|---|---|---|
yeti:select |
tabs.js |
{ tab, panel } |
Dispatched on the .tabs when a click, an arrow key, or a hash reveal selects a tab; never for the pass at load that finds no hash to reveal. |
Available since 7.0.0.