Tabs: make a real tablist (keyboard + ARIA)
Tabs can be a great way to switch between views inside a single page. The problem: many implementations are just clickable <div> elements, which breaks keyboard navigation and doesn’t communicate relationships to assistive technology.
Live demo (no tracking):
Tabs vs accordion (quick rule of thumb)
- Tabs: pick one of several peer views; usually only one is shown at a time, and switching is frequent.
- Accordion: expand/collapse content sections; users may want multiple sections open, and content is often read in sequence.
If the content is long or users may want multiple sections visible, consider an accordion instead (see: accordion note).
Semantic requirements (what makes it a real tablist)
- A container with
role="tablist". - Each tab with
role="tab", anid, andaria-controlspointing to its panel. - Selection state with
aria-selected="true/false". - Each panel with
role="tabpanel"andaria-labelledbypointing back to its tab. - Roving focus: only the active tab is in the tab order (
tabindex="0"); the rest aretabindex="-1".
Keyboard interactions
- Tab reaches the active tab (not every tab).
- ←/→ (and sometimes ↑/↓) move focus between tabs.
- Home/End jump to first/last.
Activation modes: some tablists are automatic (moving focus also selects) and some are manual (focus moves; Enter/Space selects). Pick one and make it consistent.
Common pitfalls
- Using click-only elements (divs/spans) without keyboard support.
- Leaving every tab in the tab order (too many Tab stops).
- Hiding panels visually but not updating selection state / relationships.
- Removing focus styling (users can’t tell where they are).
Minimal markup (AFTER)
Example skeleton (you still need JS to manage aria-selected, roving tabindex, and hidden):
<div role="tablist" aria-label="Example">
<button id="tab-a" role="tab" aria-selected="true" aria-controls="panel-a" tabindex="0">A</button>
<button id="tab-b" role="tab" aria-selected="false" aria-controls="panel-b" tabindex="-1">B</button>
</div>
<div id="panel-a" role="tabpanel" aria-labelledby="tab-a">...</div>
<div id="panel-b" role="tabpanel" aria-labelledby="tab-b" hidden>...</div>