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.
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Reference catalog for how to verify keyboard-navigation conformance. Pairs with the runner skills (
axe-a11y,pa11y-a11y,lighthouse-a11y).
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.
axe-a11y /
pa11y-a11y.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();
});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.
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.
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). |
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. |
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.
<label for="..."> or
aria-labelledby.(See wcag-focus-trap for the full
modal pattern.)
This matches the ARIA Menu pattern (referenced from
aria-authoring-patterns).
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-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. |
wcag-focus-trap - focus
management for modals (the intentional-trap pattern).wcag-color-contrast -
for SC 1.4.11 focus-indicator contrast.aria-authoring-patterns - for canonical interactive-widget patterns.axe-a11y,
pa11y-a11y,
lighthouse-a11y - runners that
detect SC violations programmatically.