Skip to main content
WCAG Patterns

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

  1. Unplug the mouse. Try to complete every core flow with keyboard only.
  2. Tab through the page. Every interactive control should receive focus — and you should always know where focus is (see 2.4.7 Focus Visible).
  3. Press Escape in any modal, popover, or dropdown — it should close.
  4. 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