wcag-keyboard-navigation
Reference catalog for WCAG 2.2 keyboard-navigation conformance - covers SC 2.1.1 (Keyboard), 2.1.2 (No Keyboard Trap), 2.1.4 (Character Key Shortcuts), 2.4.3 (Focus Order), 2.4.7 (Focus Visible), 2.4.11/2.4.12 (Focus Not Obscured) - with conformance levels (A/AA), test scripts, and per-criterion failure patterns. Use when authoring or reviewing keyboard-only interaction support.
wcag-keyboard-navigation
Reference catalog for how to verify keyboard-navigation conformance. Pairs with the runner skills (
axe-a11y,pa11y-a11y,lighthouse-a11y) and the adversarial agentaccessibility-code-criticthat reads this catalog when reviewing components.
Overview
WCAG 2.2 organizes accessibility into four principles - Perceivable / Operable / Understandable / Robust - at three conformance levels - A / AA / AAA (wcag22).
Keyboard navigation lives under Operable (Principle 2) and spans six key Success Criteria covering keyboard operability, focus management, and focus visibility.
When to use
Success Criteria
SC 2.1.1 - Keyboard (Level A)
Per wcag22: "All functionality of the content is operable through a keyboard interface" without requiring specific timings. Path-dependent input (handwriting) is exempt.
| Pattern | What to test |
|---|---|
<button>, <a href>, <input> | Native - already keyboard-accessible. |
<div onclick="..."> | Anti-pattern. Non-focusable; not keyboard-operable. Convert to <button>. |
Custom widget (role="button", tabindex="0") | Verify Enter / Space activates the same handler. |
| Drag-and-drop | Provide a keyboard alternative (arrow keys + Enter, or button-based reorder). |
Test script (Playwright):
test('SC 2.1.1 — interactive elements are keyboard-operable', async ({ page }) => {
// Tab to the button, activate with Enter
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await expect(page.locator('[data-testid="action-result"]')).toBeVisible();
// Repeat with Space for elements that should accept Space
await page.keyboard.press('Tab');
await page.keyboard.press('Space');
await expect(...).toBeVisible();
});SC 2.1.2 - No Keyboard Trap (Level A)
Per wcag22: "Focus must be movable away using standard methods; users should be informed of exit procedures if non- standard keys are required."
| Common failure | Fix |
|---|---|
| Modal traps Tab without an explicit Escape handler | Add Escape handler that closes the modal and restores focus. |
Embedded <iframe> (e.g. third-party widget) traps Tab | Set tabindex="-1" on the iframe OR document the exit (Esc + Tab) in surrounding label. |
| Custom date picker locks focus inside on first focus | Provide Tab to exit the calendar and continue to the next field. |
See wcag-focus-trap for the intentional-trap convention (modal focus management) which is distinct from a violation.
SC 2.1.4 - Character Key Shortcuts (Level A, added in 2.1)
Single-character shortcuts (/ to focus search, j/k to navigate items) must offer at least one of:
This SC exists because users with speech-input software produce spurious key presses; unguarded single-character shortcuts trigger unintended actions.
SC 2.4.3 - Focus Order (Level A)
Per wcag22: components must "receive focus in an order that preserves meaning and operability."
| Pattern | What to test |
|---|---|
| Reading-order vs DOM-order vs visual-order | All three should agree. CSS order / flex-direction: row-reverse can desync visual from DOM. |
tabindex="3" (positive) | Anti-pattern. Positive tabindex creates explicit focus order separate from DOM; rarely intended. Use 0 or -1. |
| Modal that doesn't trap focus | Tab moves focus to the page behind; user loses context. |
| Skip links | Always at the start of the document; tabindex="0" not needed (anchor links are focusable). |
SC 2.4.7 - Focus Visible (Level AA)
Per wcag22: "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible."
| Failure | Fix |
|---|---|
*:focus { outline: none; } without alternative | Replace with :focus-visible style: :focus-visible { outline: 2px solid var(--focus-ring); }. |
| Subtle 1px gray ring on a gray background | 3:1 contrast ratio against background per WCAG SC 1.4.11; high-contrast theme should bump to 4.5:1. |
| Disabled focus indicator on hover | Hover and focus styles are independent; never tie them. |
SC 2.4.11 / 2.4.12 - Focus Not Obscured (Level AA / AAA, NEW in 2.2)
Per wcag22: components receiving focus cannot be "entirely hidden due to author-created content."
| Failure pattern | Fix |
|---|---|
| Sticky header covers the focused input on long-form pages | Adjust scroll-margin-top so focused element scrolls into view below the sticky header. |
| Persistent cookie banner covers focused element | Modal / overlay should NOT obscure focused content; use compact banner or temporarily hide. |
| Auto-popup chat widget overlays focused fields | Render below other content OR move to a corner that doesn't intersect with form fields. |
2.4.11 (Minimum, Level AA): focus must NOT be entirely obscured.
2.4.12 (Enhanced, Level AAA): focus must NOT be partially obscured. Stricter; relevant for high-stakes forms.
Per-component test patterns
Form
Modal / dialog
(See wcag-focus-trap for the full modal pattern.)
Menu / dropdown
This matches the ARIA Menu pattern (referenced from aria-authoring-patterns).
Tabs
CI integration
The patterns above translate to per-test assertions in axe-a11y / pa11y-a11y configurations. The a11y-violation-gate skill gates the build on new violations of these SCs.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
<div onclick> for buttons | Not focusable, not Enter-activatable. | Always <button type="button">. |
outline: none for clean design | Removes focus indicator entirely (SC 2.4.7). | :focus-visible { outline: 2px solid <color>; }. |
tabindex="3" to control focus order | Creates a non-DOM order; breaks on dynamic content. | DOM-order tabbing; tabindex="0" for custom focusables, -1 to skip. |
| Single-character shortcuts always-active | Triggered by speech-input users (SC 2.1.4). | Off / remap / focus-only. |
| Sticky header obscures focused input | Violates SC 2.4.11. | scroll-margin-top per element. |