ESC

Type to search the knowledge base.

SnippetJavaScript

Tabs

WAI-ARIA tabs pattern: roles, keyboard arrows, and roving tabindex.

Explanation

Tabs need roles tablist/tab/tabpanel, aria-selected, keyboard support (Left/Right/Home/End), and only one tab in tab order at a time.

javascript
export function createTabs(root) {
  const tabs = [...root.querySelectorAll('[role="tab"]')];
  const panels = [...root.querySelectorAll('[role="tabpanel"]')];

  function select(index) {
    tabs.forEach((tab, i) => {
      const on = i === index;
      tab.setAttribute('aria-selected', String(on));
      tab.tabIndex = on ? 0 : -1;
      panels[i].hidden = !on;
    });
    tabs[index].focus();
  }

  tabs.forEach((tab, i) => {
    tab.addEventListener('click', () => select(i));
    tab.addEventListener('keydown', (e) => {
      const key = e.key;
      let next = i;
      if (key === 'ArrowRight') next = (i + 1) % tabs.length;
      else if (key === 'ArrowLeft') next = (i - 1 + tabs.length) % tabs.length;
      else if (key === 'Home') next = 0;
      else if (key === 'End') next = tabs.length - 1;
      else return;
      e.preventDefault();
      select(next);
    });
  });

  select(Math.max(0, tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true')));
  return { select };
}

Usage example

html
<div data-tabs>
  <div role="tablist" aria-label="Demo">
    <button role="tab" aria-selected="true" aria-controls="p1" id="t1">One</button>
    <button role="tab" aria-selected="false" aria-controls="p2" id="t2">Two</button>
  </div>
  <div role="tabpanel" id="p1" aria-labelledby="t1">Panel 1</div>
  <div role="tabpanel" id="p2" aria-labelledby="t2" hidden>Panel 2</div>
</div>
<script>createTabs(document.querySelector('[data-tabs]'));</script>

Related

← All snippets