WCAG 2.1.1 · Level A · WCAG 2.0
Keyboard
All functionality must be operable through a keyboard interface, without requiring specific timings for individual keystrokes.
- Principle
- Operable
- Guideline
- Keyboard Accessible
- Level
- A
- Added in
- WCAG 2.0
What it really means
Every feature must be operable through a keyboard interface, without depending on specific timing between keystrokes. Click a button with Enter or Space. Open a menu with Arrow-Down. Close a modal with Escape. Drag-only kanban cards fail. Hover-only dropdowns fail.
This criterion underpins everyone else. Screen readers, switches, voice control, head trackers, and mouth-sticks all ride on the keyboard interface.
Who it helps
- Blind screen-reader users (who never use a mouse).
- Motor-disabled users on switches, trackballs, mouth-sticks, head pointers, or voice control.
- Temporary injuries — broken arm, mouse battery dead, RSI flare-up.
- Power users — devs, designers, writers — who are always faster with the keyboard.
How to test
- Unplug the mouse. Try to complete every core flow with keyboard only.
- Tab through the page. Every interactive control should receive focus — and you should always know where focus is (see 2.4.7 Focus Visible).
- Press Escape in any modal, popover, or dropdown — it should close.
- On custom widgets (menu, tabs, tree), verify the WAI-ARIA Authoring Practices key bindings: arrow keys for selection, Home/End for first/last, typeahead for quick jump.
Common failing patterns
Divs masquerading as buttons. A <div onClick={…}> is invisible to
Tab:
// Fails: no keyboard, no role, no semantics.
<div className="button" onClick={handleSave}>Save</div>Hover-only dropdowns. If the menu only opens on :hover, keyboard
users can't reach the submenu.
Drag-only interactions. Kanban boards, file-upload zones, sliders — all must have a keyboard alternative (see 2.5.7 Dragging Movements).
Auto-opening on focus. A select that opens its listbox when focused is a change of context on focus — also fails 3.2.1 On Focus.
A passing pattern
// Real button — native keyboard support for Enter and Space.
<button type="button" onClick={handleSave}>Save</button>
// Custom disclosure — button toggles aria-expanded.
function Disclosure({ title, children }: Props) {
const [open, setOpen] = useState(false);
return (
<>
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
>
{title}
</button>
{open && <div>{children}</div>}
</>
);
}For custom widgets with complex keyboard behaviour (menu, listbox, grid), lean on battle-tested libraries: Radix UI, React Aria, Ariakit, Headless UI on React; Angular CDK a11y on Angular; Melt UI or Svelte-Headless on Svelte. Don't hand-roll keyboard handlers unless you have to — there are a lot of subtle rules (focus-visible vs focused, typeahead debounce, Home/End scoping).
Notes and edge cases
- Path-dependent input is exempt: freehand drawing, virtual instruments. If you need to be AAA, you need 2.1.3 Keyboard (No Exception) — no exception.
- Custom keyboard shortcuts must be remappable or disableable (2.1.4 Character Key Shortcuts).
Related rules
- axe: tabindex — positive tabindex values.
- axe: focus-order-semantics — focus stops on unlabelled widgets.
- axe: scrollable-region-focusable — overflow regions unreachable by keyboard.
- WCAG 2.1.2 No Keyboard Trap.
- WCAG 2.4.3 Focus Order.