Accessibility Certified – Home

Tabs: switch panels with the arrow keys, not a tabbing marathon

Tabs show one of several panels at a time. They are accessible when the tab list is a single tab stop and the arrow keys move between tabs.

4.1.22.1.12.4.31.4.11.4.11

Live demo

This demo is checked with axe-core without violations and fully operable by keyboard.

Try it with the keyboard: Tab, Enter, Space, the arrow keys and Escape. With a screen reader on, you hear what gets announced.

A 20-litre backpack, water-repellent, with a padded laptop sleeve.

The live demo needs JavaScript. Code and requirements are below.

Keyboard interaction

Tab
Moves into the tab list onto the active tab; pressed again, into the panel.
Left / Right arrow
Moves to the previous or next tab and shows it (wraps around at the ends).
Home / End
Moves to the first or last tab.

When to use this pattern

Tabs suit content of equal weight that people do not need to see at the same time, such as product details, specifications and delivery. If visitors need the content in sequence or want to compare it, a normal page with subheadings is often better.

Tabs are not navigation: if an item takes you to another page, it is a link in a <nav>, not a tab.

Roles, states and properties

  • A container with role="tablist" and a name via aria-label or aria-labelledby.
  • Each tab is a <button role="tab"> with aria-selected and aria-controls.
  • Only the active tab has tabindex="0"; all others have tabindex="-1" (roving tabindex).
  • Each panel has role="tabpanel", aria-labelledby pointing to its tab, and tabindex="0" if it contains nothing focusable.
  • Inactive panels are hidden with hidden.

Keyboard interaction

KeyEffect
TabMoves into the tab list onto the active tab; pressed again, into the panel.
Left / Right arrowMoves to the previous or next tab and shows it (wraps around at the ends).
Home / EndMoves to the first or last tab.

Code

HTML and JavaScript (shortened)
<div role="tablist" aria-label="Product info">
  <button role="tab" id="t1" aria-selected="true"
    aria-controls="p1">Details</button>
  <button role="tab" id="t2" aria-selected="false"
    aria-controls="p2" tabindex="-1">Delivery</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1"
  tabindex="0">…</div>
<div role="tabpanel" id="p2" aria-labelledby="t2"
  tabindex="0" hidden>…</div>

// Arrow keys: tabs[i].focus(), then select(i)
list.addEventListener('keydown', e => {
  const n = { ArrowRight: i + 1, ArrowLeft: i - 1,
    Home: 0, End: tabs.length - 1 }[e.key];
  if (n === undefined) return;
  e.preventDefault();
  select((n + tabs.length) % tabs.length);
});

Common mistakes

  • Every tab is its own tab stop, so moving past the widget means tabbing through all of them.
  • Tab roles without arrow-key support: screen readers announce tabs that then do not behave like tabs.
  • Active tab marked by colour only, without aria-selected and without a second visual cue.
  • Links that change the page marked up as tabs.

Related WCAG criteria

Sources

Are your components built like this?

The free check finds many issues automatically. In the audit a person tests every component with a keyboard and a screen reader.

Free checkSee our method