title: Keyboard and focus — WCAG operable applies_to: [web, all-ui] version: 1.0.0 last_updated: 2026-05 stability: stable
Keyboard and focus¶
Every interactive element on screen must be operable by keyboard alone, with a visible focus indicator at every step. This is non-negotiable for AA compliance and for ~7% of users.
Tab order rules¶
- Logical reading order. Tab follows the visual reading order — top-to-bottom, left-to-right (or RTL equivalent).
- Skipping with
tabindex="-1": removes the element from the tab order but keeps it focusable programmatically. Use for items focused by code (e.g., modal title focused on open). - Never use
tabindex≥ 1. It overrides natural order and creates inconsistencies. - Hidden/disabled elements:
display:none,visibility:hidden,disabled— auto-removed. - Decorative interactive-looking divs: must have
roleandtabindex="0"to be reachable, OR not be interactive.
Focus indicators¶
Required by WCAG 2.4.11 (AA in 2.2).
| Property | Minimum |
|---|---|
| Thickness | 2 CSS px outline OR equivalent area |
| Contrast | 3:1 against adjacent colors (the element's normal color AND its background) |
| Coverage | Encloses the element fully OR fills it with a 3:1+ contrast change |
/* Good — works on light and dark backgrounds */
button:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Better for design systems — token-driven */
button:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
/* DO NOT do this */
*:focus { outline: none; } /* removes a11y, fails 2.4.7 */
:focus-visible (modern browsers) keeps focus rings on keyboard nav but hides them for mouse clicks — users who don't want them are not disturbed, keyboard users are protected.
Required keyboard interactions per pattern¶
| Component | Keys |
|---|---|
| Button | Enter, Space activate. |
| Link | Enter activates. (Not space — that scrolls.) |
| Menu / Dropdown | ↑/↓ move, Enter activate, Escape close, Home/End first/last. |
| Tabs | ←/→ move (↑/↓ if vertical), Home/End first/last, Enter/Space activate (or auto-activate for some patterns). |
| Combobox | ↓ open, ↑/↓ navigate options, Enter select, Escape close, type-ahead supported. |
| Modal / Dialog | Open: focus moves into dialog. Escape closes. Focus is trapped until close. On close: focus returns to the trigger. |
| Slider | ←/↓ decrement, →/↑ increment, Home min, End max, PageUp/Down larger step. |
| Tree | ↑/↓ move, ← collapse / move to parent, → expand / move to first child, Enter activate. |
| Toggle / Switch | Space toggles. (Not Enter unless <button>-typed.) |
| Date picker | Within calendar grid: arrow keys move days, PageUp/Down months, Shift+PgUp/Dn years. |
Source: W3C ARIA Authoring Practices Guide — the canonical reference for keyboard patterns.
Focus management on route change (SPA)¶
When a client-side route changes:
- Move focus to the new page's <h1> (or main landmark).
- Update document title (so screen reader announces the new context).
- Without this, screen reader users hear nothing — the route change is invisible.
// On route change
useEffect(() => {
document.title = newTitle;
document.querySelector('h1')?.focus();
}, [route]);
Touch target sizing¶
WCAG 2.5.5 (Level AAA) and 2.5.8 (Level AA in 2.2) require:
| Standard | Minimum |
|---|---|
| WCAG 2.2 AA (Target Size — Minimum) | 24×24 CSS px |
| WCAG 2.1 AAA (Target Size) | 44×44 CSS px |
| iOS HIG | 44×44 pt |
| Material guidelines | 48×48 dp |
For consumer mobile UIs, target 44×44 minimum. Smaller is allowed only when: - Equivalent target available elsewhere on the page, OR - Inline within a sentence (e.g., link in body text).
Spacing matters too: a 24×24 button surrounded by enough space to count as a 44×44 hit area is better than a tightly-packed grid of 32×32 buttons.
Common failures to flag in review¶
<div onClick>with noroleand no tab support.- Placeholder used as label (
<input placeholder="Email">with no<label>). - Modal that steals focus on open but doesn't return it on close.
Escapeon dropdown closes the page modal too (event bubbling).- Custom select with no type-ahead.
- Focus ring removed globally with
outline: none. - Carousel that auto-advances and traps focus.