ESC

Type to search the knowledge base.

Accessible Tabs Pattern

APG tabs with tablist/tab/tabpanel, arrow key navigation, aria-selected, and focus management without losing the active panel.

intermediate3 min read
  • accessibility
  • accessible-tabs

Tabs switch between peer panels in one view. Accessibility needs correct roles, keyboard model, and state (aria-selected, aria-controls). A row of buttons that only works with a mouse is not a tab pattern.

Docs: APG Tabs, MDN tab role.

Structure

<div role="tablist" aria-label="Account">
  <button
    role="tab"
    id="tab-profile"
    aria-selected="true"
    aria-controls="panel-profile"
    tabindex="0"
  >
    Profile
  </button>
  <button
    role="tab"
    id="tab-billing"
    aria-selected="false"
    aria-controls="panel-billing"
    tabindex="-1"
  >
    Billing
  </button>
</div>

<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
  …
</div>
<div role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" hidden>
  …
</div>
  • Only one tab aria-selected="true"
  • Roving tabindex: selected tab 0, others -1
  • Panels labeled by their tabs

Keyboard

Key Behavior
Tab Enter/leave tablist (one stop for the active tab)
Left/Right (horizontal) Move selection/focus across tabs
Home/End First/last
Space/Enter Activate if activation is manual

APG documents automatic vs manual activation. Automatic: arrow changes panel immediately. Manual: arrow moves focus; Enter selects.

Focus notes

When activating a tab, focus usually stays on the tab. Users Tab into the panel content. Don’t force focus into the panel unless a specific UX needs it (and then be consistent).

Do you need tabs?

If content is separate pages, use links/routing. Tabs are for in-place peer panels. For mobile, sometimes a select or stacked sections works better.

React state sketch

function Tabs({ items }: { items: { id: string; label: string; panel: React.ReactNode }[] }) {
  const [active, setActive] = useState(items[0].id);
  return (
    <>
      <div role="tablist" aria-label="Sections">
        {items.map((item) => (
          <button
            key={item.id}
            role="tab"
            type="button"
            aria-selected={active === item.id}
            aria-controls={`panel-${item.id}`}
            id={`tab-${item.id}`}
            tabIndex={active === item.id ? 0 : -1}
            onClick={() => setActive(item.id)}
          >
            {item.label}
          </button>
        ))}
      </div>
      {items.map((item) => (
        <div
          key={item.id}
          role="tabpanel"
          id={`panel-${item.id}`}
          aria-labelledby={`tab-${item.id}`}
          hidden={active !== item.id}
        >
          {item.panel}
        </div>
      ))}
    </>
  );
}

Add onKeyDown for arrows in production.

Footguns

  1. All tabs in tab order (tabindex=0 on every tab).
  2. Panels not hidden correctly to AT.
  3. Missing aria-controls / ids.
  4. Nested interactive tabs without keyboard support.

Interview out-loud answer

“Tabs use tablist/tab/tabpanel with aria-selected and roving tabindex. Arrow keys move between tabs; Tab moves into the page. I hide inactive panels and wire aria-controls. For real navigation between pages I use links instead.”

Dynamic tabs

When tabs can be added/removed (browser-like UIs), manage focus when the active tab is removed — move to the previous or next tab and update aria-selected. Don’t leave aria-controls pointing at detached panels.

Extra practice

Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.

Notes from real codebases

Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.

Further reading

Related guides