← Recipes

阅读中文版本

Build an Accessible Tabs Component

Create a tabs widget with correct ARIA roles, roving tabindex, and arrow-key navigation so keyboard and screen-reader users can switch panels reliably.

~45 minaccessibilityfrontendaria

Task

Implement a tabs component that meets the WAI-ARIA authoring practices without a heavy dependency.

When to use this

A custom tabs widget looks right but cannot be operated by keyboard and exposes no roles, so assistive technology treats panels as unrelated blocks.

Prerequisites

  • A component framework with event handling
  • Familiarity with ARIA roles and attributes

Steps

1. Give the list role tablist, each trigger role tab, and each panel role tabpanel with aria-labelledby.

Expected output: Assistive technology announces tabs and their panels correctly.

2. Use roving tabindex so only the active tab is in the tab order.

npx @axe-core/cli http://localhost:3000 --exit

Expected output: Tab moves onto the widget once instead of through every tab.

3. Handle ArrowLeft/ArrowRight to move focus and Home/End to jump, selecting on activation or focus.

Expected output: Keyboard users can switch panels without a mouse.

4. Wire aria-selected and hide inactive panels from the accessibility tree.

Expected output: Only the active panel content is exposed.

Troubleshooting

Problem

Focus lost after switching

Solution

Move focus to the newly selected tab and keep tabindex in sync so it stays reachable.

Problem

Panel content reachable incorrectly

Solution

Hide inactive panels with hidden or display none, not only aria-hidden, so their focusables are unreachable.

Limitations

  • Auto-activation on arrow keys can be surprising for panels that load expensive content; manual activation is safer then.
  • This pattern does not cover vertical or wrap-around tabs, which need extra key handling.

Context

Implement a tabs component that meets the WAI-ARIA authoring practices without a heavy dependency. A custom tabs widget looks right but cannot be operated by keyboard and exposes no roles, so assistive technology treats panels as unrelated blocks.

Steps

  1. Give the list role tablist, each trigger role tab, and each panel role tabpanel with aria-labelledby. Expected: Assistive technology announces tabs and their panels correctly.
  2. Use roving tabindex so only the active tab is in the tab order. Expected: Tab moves onto the widget once instead of through every tab.
  3. Handle ArrowLeft/ArrowRight to move focus and Home/End to jump, selecting on activation or focus. Expected: Keyboard users can switch panels without a mouse.
  4. Wire aria-selected and hide inactive panels from the accessibility tree. Expected: Only the active panel content is exposed.
<ul role="tablist">
  <li role="tab" aria-selected="true" tabindex="0">Overview</li>
  <li role="tab" aria-selected="false" tabindex="-1">Settings</li>
</ul>
<div role="tabpanel" aria-labelledby="tab-overview">…</div>

Verify

  • Assistive technology announces tabs and their panels correctly.
  • Tab moves onto the widget once instead of through every tab.
  • Keyboard users can switch panels without a mouse.
  • Only the active panel content is exposed.

Limits

  • Auto-activation on arrow keys can be surprising for panels that load expensive content; manual activation is safer then.
  • This pattern does not cover vertical or wrap-around tabs, which need extra key handling.