Home ARIA

ARIA tabs pattern

The tabs pattern combines a tablist of tab controls with corresponding tabpanels. Choose between manual and automatic activation; both are spec-compliant but have different keyboard semantics.

What the tabs pattern is

A tabs widget is a row of activator controls (the tablist) where each activator (a tab) reveals a corresponding tabpanel of content. Only one panel is visible at a time. The pattern is one of the most-used composite widgets on the web — about 14% of unique pages on the HTTP Archive 2024 report include a tabs pattern.

<div role="tablist" aria-label="Settings">
  <button role="tab" id="tab-1" aria-selected="true"  aria-controls="panel-1" tabindex="0">Profile</button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Notifications</button>
  <button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1">Privacy</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">…</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>…</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>…</div>

Key requirements:

  • role="tablist" on the container; an aria-label or aria-labelledby if multiple tablists exist on the page.
  • role="tab" on each activator; aria-selected="true" on the current tab, "false" on others.
  • aria-controls on each tab pointing to the panel id.
  • Roving tabindex: only the current tab has tabindex="0"; others have tabindex="-1".
  • role="tabpanel" on each panel; aria-labelledby referencing the tab id; non-current panels hidden via the hidden attribute (preferred) or display: none.

Manual vs automatic activation

Two valid sub-patterns differ in when the panel changes:

Sub-patternTrigger to switch panelKeyboard semantics
AutomaticArrow-key aloneUp/Down (vertical) or Left/Right (horizontal) navigates and activates.
ManualArrow-key + Enter or SpaceArrow-key moves focus; Enter / Space activates.

The APG recommends automatic when each tab’s panel is cheap to render (e.g., already in the DOM). It recommends manual when the panel is heavy to render or fetches data — automatic activation would trigger a network request on every arrow key.

About 65% of production tabs widgets surveyed in 2024 use automatic; large dashboards (Datadog, Grafana, etc.) use manual.

Keyboard interaction

KeyEffect (automatic)Effect (manual)
TabMove focus to the active tab; if focus already on the tablist, leave to next focusable.Same.
Right ArrowActivate next tab; wrap to first if at end.Move focus to next tab.
Left ArrowActivate previous tab; wrap to last if at start.Move focus to previous tab.
HomeActivate / focus first tab.Same.
EndActivate / focus last tab.Same.
Enter / Space(no effect; arrow already activated)Activate the focused tab.

For vertical tablists, swap Up/Down for Left/Right.

Common pitfalls

  • No roving tabindex. Every tab tabindex="0"; Tab walks through all of them, breaking the keyboard model.
  • aria-selected not synced with tabindex. Selected tab must be the only tabindex="0".
  • Tab panels in the DOM but visually hidden via opacity. The hidden attribute (or display: none) is required so AT does not announce hidden content.
  • Heavy panel content rendered automatically. Use manual activation, or load the panel lazily on first activation.
  • <a href> instead of <button> for tabs. Permitted but the link follows on click in some engines, navigating away.
  • Multiple tablists without aria-label. Two tablists with identical “tab 1, tab 2” announcements confuse screen-reader users.

Cross-engine support

The pattern is implemented at the markup layer; engines just have to expose role="tab" and friends to AX. Mapping pass-rate is about 99% across Chromium (Blink), WebKit, and Gecko per wpt.fyi/wai-aria as of 2024.

Authoring tips

  • Wire focus management early. Most tab bugs are focus-management bugs disguised as ARIA bugs.
  • Persist the active tab in the URL hash for deep links: #panel=privacy. The pattern survives a refresh.
  • For tablists that overflow horizontally on narrow viewports, ensure arrow-key navigation still works inside an overflow-x: auto container; auto-scroll the active tab into view with scrollIntoView({ inline: 'nearest' }).
  • Avoid mixing tabs and accordions in the same component: the responsive transformation breaks the ARIA model. Prefer two separate components selected by media query.

Further reading