Tabs
Semantic ARIA-first tabs styling using role=tablist, role=tab, and role=tabpanel.
Overview
Tabs are styled semantically from ARIA roles rather than a required custom component wrapper.
Interactive ARIA tabs
Try the arrow, Home, and End keys while a tab is focused.
<div data-tabs-example>
<div role="tablist" aria-label="Account sections">
<button role="tab" id="profile-tab" aria-controls="profile-panel" aria-selected="true">Profile</button>
<button role="tab" id="security-tab" aria-controls="security-panel" aria-selected="false" tabindex="-1">Security</button>
<button role="tab" id="billing-tab" aria-controls="billing-panel" aria-selected="false" tabindex="-1">Billing</button>
</div>
<div role="tabpanel" id="profile-panel" aria-labelledby="profile-tab">
Update your public name, avatar, and biography.
</div>
<div role="tabpanel" id="security-panel" aria-labelledby="security-tab" hidden>
Manage your password, passkeys, and active sessions.
</div>
<div role="tabpanel" id="billing-panel" aria-labelledby="billing-tab" hidden>
Review invoices and change your payment method.
</div>
</div> Required Roles
role="tablist"role="tab"role="tabpanel"
Basic Example
<div role="tablist" aria-label="Demo tabs"> <button role="tab" aria-selected="true" aria-controls="panel-a" id="tab-a">Overview</button> <button role="tab" aria-selected="false" aria-controls="panel-b" id="tab-b" tabindex="-1">API</button></div>
<div role="tabpanel" id="panel-a" aria-labelledby="tab-a">Overview content</div><div role="tabpanel" id="panel-b" aria-labelledby="tab-b" hidden>API content</div>States
- active:
aria-selected="true" - disabled:
aria-disabled="true"ordisabled - hidden panels:
hidden
Optional Selector Aliases
[data-tablist],.tablist[data-tab],.tab[data-tabpanel],.tabpanel
Tokens
--tabs-gap--tabs-padding-inline--tabs-padding-block--tabs-border-color--tabs-background--tabs-tab-color--tabs-tab-hover-background--tabs-tab-hover-color--tabs-tab-active-background--tabs-tab-active-color--tabs-tab-active-border--tabs-panel-background--tabs-panel-border--tabs-panel-radius--tabs-panel-padding--tabs-indicator-width
Notes
- CSS styles the ARIA states; JavaScript should manage selection, focus movement, and panel visibility.
- Vertical tablists are supported with
aria-orientation="vertical".
Behavior checklist
- Exactly one tab should have
aria-selected="true"andtabindex="0". - Inactive tabs should use
aria-selected="false"andtabindex="-1". - Each tab’s
aria-controlsmust match its panelid. - Each panel’s
aria-labelledbymust match its tabid. - Left/Right Arrow, Home, and End should move focus for a horizontal tablist.
- Toggle the panel’s
hiddenattribute when selection changes.
The live example above includes this behavior. CSS alone intentionally does not change ARIA state.
No-JavaScript option
When the content must remain interactive without JavaScript, use the native
disclosure-driven pattern instead of assigning ARIA tab roles without their
required behavior. A shared name keeps one panel open at a time in supporting
browsers; elsewhere it remains a useful group of ordinary disclosures.
Native disclosure tabs
Disable JavaScript and the summaries still open their associated content.
<div data-tabs-details="true" style="--tab-count: 3">
<details name="tabs-no-js" open style="--n: 1">
<summary>Overview</summary>
<p>Native details provide activation, focus behavior, and open state.</p>
</details>
<details name="tabs-no-js" style="--n: 2">
<summary>API</summary>
<p>Set <code>—tab-count</code> on the host and <code>—n</code> on each item.</p>
</details>
<details name="tabs-no-js" style="--n: 3">
<summary>Fallback</summary>
<p>The same markup falls back to stacked disclosure widgets.</p>
</details>
</div> Overview
Native details provide activation, focus behavior, and open state.
API
Set —tab-count on the host and —n on each item.
Fallback
The same markup falls back to stacked disclosure widgets.
Use formal ARIA tabs when arrow-key tab semantics are required. Use this native pattern when resilient no-script interaction is the priority.