Accessibility pattern · Navigation

Tabs

The tab list is one stop in the Tab order; the arrow keys move between tabs inside it. Each tab names its panel, and the selected one says so, so a screen reader user knows where they are and how many tabs there are.

WCAG criteria
5
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Your name and photo appear on everything you share.

Name
Asha Rao
Handle
@asha

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves into the tab list, onto the selected tab; the next Tab moves into its panel.
Arrow Right or Arrow LeftMoves to the next or previous tab, wrapping at the ends, and shows its panel.
Home or EndMoves to the first or last tab.
Enter or SpaceShows the focused tab's panel, when tabs are set to activate with Enter.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the tab listAccount settings, tab list. Profile, tab, selected, 1 of 4
Arrow Right moves to the next tabSecurity, tab, selected, 2 of 4
Tab moves into the panelSecurity, tab panel

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-tabs" data-ap-tabs>
  <div class="ap-tabs__list" role="tablist" aria-label="Account settings">
    <button type="button" class="ap-tabs__tab" role="tab" id="tab-profile" aria-selected="true" aria-controls="panel-profile">Profile</button>
    <button type="button" class="ap-tabs__tab" role="tab" id="tab-security" aria-selected="false" aria-controls="panel-security" tabindex="-1">Security</button>
    <button type="button" class="ap-tabs__tab" role="tab" id="tab-billing" aria-selected="false" aria-controls="panel-billing" tabindex="-1">Billing</button>
    <button type="button" class="ap-tabs__tab" role="tab" id="tab-alerts" aria-selected="false" aria-controls="panel-alerts" tabindex="-1">Alerts</button>
    <span class="ap-tabs__ink" aria-hidden="true"></span>
  </div>

  <div class="ap-tabs__panel" role="tabpanel" id="panel-profile" aria-labelledby="tab-profile" tabindex="0">
    <p class="ap-tabs__lead">Your name and photo appear on everything you share.</p>
    <dl class="ap-tabs__facts">
      <div><dt>Name</dt><dd>Asha Rao</dd></div>
      <div><dt>Handle</dt><dd>@asha</dd></div>
    </dl>
  </div>
  <div class="ap-tabs__panel" role="tabpanel" id="panel-security" aria-labelledby="tab-security" tabindex="0" hidden>
    <p class="ap-tabs__lead">Two-step sign-in is on. You last changed your password 3 months ago.</p>
    <dl class="ap-tabs__facts">
      <div><dt>Two-step sign-in</dt><dd>Authenticator app</dd></div>
      <div><dt>Active sessions</dt><dd>2 devices</dd></div>
    </dl>
  </div>
  <div class="ap-tabs__panel" role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabindex="0" hidden>
    <p class="ap-tabs__lead">You are on the yearly plan. It renews on 12 March.</p>
    <dl class="ap-tabs__facts">
      <div><dt>Plan</dt><dd>Studio, yearly</dd></div>
      <div><dt>Card</dt><dd>Ends in 4242</dd></div>
    </dl>
  </div>
  <div class="ap-tabs__panel" role="tabpanel" id="panel-alerts" aria-labelledby="tab-alerts" tabindex="0" hidden>
    <p class="ap-tabs__lead">We email you about security and billing only.</p>
    <dl class="ap-tabs__facts">
      <div><dt>Security alerts</dt><dd>On</dd></div>
      <div><dt>Product news</dt><dd>Off</dd></div>
    </dl>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The tab list, tabs and panels are tied together with roles and aria-controls, so the structure is exposed, not just drawn.

  • 2.1.1 Keyboard Level A

    The arrow keys, Home and End move between tabs; nothing needs a pointer.

  • 2.4.3 Focus Order Level A

    Only the selected tab is in the Tab order, so the next Tab goes straight into its panel instead of through every tab.

  • 2.4.7 Focus Visible Level AA

    The focused tab shows a ring that clears 3:1 against the tab list.

  • 4.1.2 Name, Role, Value Level A

    Each tab has role=tab and aria-selected, and the panel is labelled by its tab.

Usage

When to use it

Use it

  • Switching between views of the same thing, like the sections of an account or the formats of an export.
  • Content people compare a section at a time rather than read top to bottom.

Use something else

  • Navigation to other pages: use links in a nav landmark, which can be opened in new tabs and bookmarked.
  • More tabs than fit on a phone in one row; consider a select or an accordion there.
  • Steps people must do in order: use a multi-step form with progress steps.

Common failures

How it usually goes wrong

  • Tabs that are links to other pages

    role=tab on page links breaks their meaning: people expect a tab to change the panel below, not to load a new page.

  • Every tab in the Tab order

    With ten tabs that is ten key presses before the content. One stop for the list, with arrow keys inside it, is the expected model.

  • Selection shown only by color

    An underline in a slightly different shade is invisible to many. The selected tab here also changes weight and sits on a solid indicator.

  • Panels without a name

    A tabpanel with no aria-labelledby is announced as just "tab panel". Pointing it at its tab names it.

  • Hidden panels left in the accessibility tree

    Panels hidden only by moving them off screen are still read out. The hidden attribute removes them for everyone.

Notes

Building it

  • Automatic activation suits panels that appear instantly. If showing a panel loads data, use manual activation (the option above) so arrowing past a tab does not fire requests.
  • The panel has tabindex=0 so keyboard users can reach it even when it starts with plain text; drop it if every panel begins with a link or a field.
  • Name the tab list with aria-label or aria-labelledby; it is the first thing announced when focus arrives.
  • The sliding indicator is decoration: it is aria-hidden, and it stops moving when the system asks for reduced motion.

Sources: WAI-ARIA Authoring Practices: Tabs

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction