Skip to content

Find what you need, instantly

Search components, ARIA roles & attributes, and WCAG criteria.

NavigationFlagship guide

Tabs

A set of layered sections of content, one visible at a time, selected via a tab list.

Last verified against WCAG 2.2 and WAI-ARIA APG 1.2 on 2026-07-02.

Implementation

Manage your name, photo, and public bio.
tabs-pattern.tsx
function TabsPattern({ activationMode = "automatic" }) {
  const [selected, setSelected] = useState(TABS[0].id);
  const [focused, setFocused] = useState(TABS[0].id);
  const tabRefs = useRef({});

  function moveFocus(index) {
    const tab = TABS[index];
    setFocused(tab.id);
    tabRefs.current[tab.id]?.focus();
    // Automatic activation: selecting follows focus immediately.
    if (activationMode === "automatic") setSelected(tab.id);
  }

  function onKeyDown(e) {
    const i = TABS.findIndex(t => t.id === focused);
    if (e.key === "ArrowRight") { e.preventDefault(); moveFocus((i + 1) % TABS.length); }
    if (e.key === "ArrowLeft")  { e.preventDefault(); moveFocus((i - 1 + TABS.length) % TABS.length); }
    if (e.key === "Home") { e.preventDefault(); moveFocus(0); }
    if (e.key === "End")  { e.preventDefault(); moveFocus(TABS.length - 1); }
    // Manual activation: arrowing only moves focus; Enter/Space commits it.
    if (activationMode === "manual" && (e.key === "Enter" || e.key === " ")) {
      e.preventDefault();
      setSelected(focused);
    }
  }

  return (
    <div role="tablist" aria-label="Account settings" onKeyDown={onKeyDown}>
      {TABS.map(tab => (
        <button
          key={tab.id}
          ref={el => (tabRefs.current[tab.id] = el)}
          role="tab"
          id={`tab-${tab.id}`}
          aria-selected={tab.id === selected}
          aria-controls={`panel-${tab.id}`}
          // Roving tabindex: only the focused tab is Tab-reachable.
          tabIndex={tab.id === focused ? 0 : -1}
          onClick={() => { setFocused(tab.id); setSelected(tab.id); }}
        >
          {tab.label}
        </button>
      ))}
      {TABS.map(tab => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={tab.id !== selected}
          tabIndex={0}
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

Live demo

Manage your name, photo, and public bio.

Required roles, states & properties

Required roles, states, and properties
ElementAttributeWhy
Tab list wrapperrole="tablist"Identifies the group of tab buttons as a tablist, so AT announces the total count and enables tab-specific navigation commands.
Each tabrole="tab" + aria-selectedIdentifies each button as a tab and communicates which one is currently active — announced as "selected."
Each tabaria-controlsAssociates the tab with the panel id it reveals.
Each tabtabIndex (roving)Only the active/focused tab has tabIndex=0; the rest are -1, so Tab moves from the tablist straight to panel content instead of stopping on every tab.
Each panelrole="tabpanel" + aria-labelledbyIdentifies the panel and labels it via the tab that reveals it, so its accessible name matches the tab label.

Keyboard interaction model

Keyboard interaction model
KeyBehavior
TabMoves focus from the page into the tablist (landing on the active tab), then out to the active panel's content.
Right / Left ArrowMoves focus to the next / previous tab, wrapping at the ends. In automatic mode, also activates it.
Home / EndMoves focus to the first / last tab.
Enter / SpaceIn manual activation mode, activates the focused tab. Not needed in automatic mode.

Focus management rules

  • Tab into the widget lands on the currently active tab, never on an inactive one.
  • Arrow keys move focus between tabs using a roving tabindex — never real DOM focus loss.
  • Tab out of the tablist moves focus directly to the active panel's content, skipping inactive tabs entirely.

WCAG 2.2 success criteria mapping

WCAG 2.2 success criteria that apply to this component
SCNameLevelWhy it applies
2.1.1KeyboardAAll functionality is operable through a keyboard interface with no specific timing.
2.4.3Focus OrderAFocusable components receive focus in an order that preserves meaning and operability.
3.2.1On FocusAReceiving focus does not trigger an unexpected change of context.
4.1.2Name, Role, ValueAFor all UI components, name, role, and value are programmatically determinable; states and changes are announced.

References