⚡IntermediateAdvanced Accessibility

Keyboard Navigation Patterns

Implement ARIA patterns for keyboard navigation in tabs, accordions, menus, and tree views.

Overview

The WAI-ARIA Authoring Practices Guide (APG) defines keyboard interaction patterns for common UI components. These patterns establish consistent, predictable keyboard behavior that users learn once and apply everywhere. Key patterns covered here: - Tabs: Tab to the tab panel, arrows to switch tabs - Accordion: Tab between headers, Enter/Space to expand - Menu button: Enter/Space to open, arrows to navigate, Escape to close - Roving tabindex: the technique of moving tab focus through a component The principle: use Tab to move between components; use arrow keys to navigate within a component.

Key Points

  • •Tab moves focus between components; Arrow keys navigate within a component
  • •Roving tabindex: only one item has tabindex="0" at a time in a group
  • •Enter/Space activate buttons and open menus
  • •Escape closes menus, dropdowns, dialogs
  • •Home/End jump to first/last item in a list/menu
  • •aria-selected="true" marks active tab; aria-expanded="true" marks open accordion

Examples

Tab component with ARIA and roving tabindex

<div role="tablist" aria-label="Content sections">
  <button role="tab" id="tab-1" aria-controls="panel-1" aria-selected="true"  tabindex="0">HTML</button>
  <button role="tab" id="tab-2" aria-controls="panel-2" aria-selected="false" tabindex="-1">CSS</button>
  <button role="tab" id="tab-3" aria-controls="panel-3" aria-selected="false" tabindex="-1">JS</button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <p>HTML content</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <p>CSS content</p>
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
  <p>JS content</p>
</div>

<script>
const tabs = [...document.querySelectorAll('[role="tab"]')];

tabs.forEach((tab, i) => {
  tab.addEventListener('keydown', (e) => {
    let newIndex;
    if (e.key === 'ArrowRight') newIndex = (i + 1) % tabs.length;
    if (e.key === 'ArrowLeft')  newIndex = (i - 1 + tabs.length) % tabs.length;
    if (e.key === 'Home')       newIndex = 0;
    if (e.key === 'End')        newIndex = tabs.length - 1;
    if (newIndex !== undefined) {
      e.preventDefault();
      activateTab(newIndex);
    }
  });
});

function activateTab(index) {
  tabs.forEach((t, i) => {
    const selected = i === index;
    t.setAttribute('aria-selected', selected);
    t.tabIndex = selected ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !selected;
    if (selected) t.focus();
  });
}
</script>

Accordion keyboard pattern

<div id="accordion">
  <h3>
    <button
      aria-expanded="false"
      aria-controls="sect1"
      id="accordion-1"
    >What is HTML?</button>
  </h3>
  <div id="sect1" role="region" aria-labelledby="accordion-1" hidden>
    <p>HTML is the foundation of web pages.</p>
  </div>

  <h3>
    <button
      aria-expanded="false"
      aria-controls="sect2"
      id="accordion-2"
    >What is CSS?</button>
  </h3>
  <div id="sect2" role="region" aria-labelledby="accordion-2" hidden>
    <p>CSS controls visual presentation.</p>
  </div>
</div>

<script>
document.querySelectorAll('#accordion button').forEach(btn => {
  btn.addEventListener('click', () => {
    const expanded = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', !expanded);
    document.getElementById(btn.getAttribute('aria-controls')).hidden = expanded;
  });
});
</script>

Common Mistakes

  • Using only Tab for navigation within a component arrow keys are expected for list-like components
  • Forgetting to update aria-selected/aria-expanded state attributes must match visible state
  • Not implementing Escape key to close menus/dropdowns

Note

Bookmark the WAI-ARIA Authoring Practices Guide at w3.org/WAI/ARIA/apg. It has interactive examples for every common component pattern with source code.

Exercises

Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is "roving tabindex"?