DEV Community

DevToolsmith
DevToolsmith

Posted on

Keyboard Navigation Testing: A Developer Complete Guide to WCAG Operability

Keyboard accessibility is one of the most important — and most neglected — aspects of web accessibility. An estimated 2.5 million Americans have motor disabilities that prevent mouse use. If your site can't be operated entirely by keyboard, you're excluding them completely.

The Four Core Principles

WCAG 2.2 Principle 2 (Operable) contains the keyboard requirements:

  • 2.1.1 Keyboard (AA): All functionality must be operable via keyboard
  • 2.1.2 No Keyboard Trap (AA): If focus moves into a component, it must be possible to move it out
  • 2.4.3 Focus Order (AA): If page can be navigated sequentially, order must be logical and predictable
  • 2.4.7 Focus Visible (AA): Any keyboard-operable UI must have a visible focus indicator
  • 2.4.11 Focus Appearance (AA, new in 2.2): Focus indicator must meet size and contrast requirements

Testing Without Automated Tools

Start with the basic keyboard test:

  1. Unplug (or ignore) your mouse
  2. Press Tab to move forward through interactive elements
  3. Press Shift+Tab to move backward
  4. Use Enter/Space to activate buttons, links, checkboxes
  5. Use arrow keys for radio groups, menus, sliders
  6. Use Escape to close dialogs and menus

Any element you can't reach or activate? That's a WCAG 2.1.1 failure.

The Most Common Keyboard Failures

Custom dropdowns and menus

// ❌ Keyboard inaccessible
function Dropdown({ items }) {
  return (
    <div onClick={toggle} className="dropdown">
      {items.map(item => (
        <div onClick={() => select(item)}>{item.label}</div>
      ))}
    </div>
  );
}

// ✅ Fully keyboard accessible
function Dropdown({ items }) {
  return (
    <div
      role="combobox"
      aria-haspopup="listbox"
      aria-expanded={isOpen}
      tabIndex={0}
      onKeyDown={handleKeyDown} // handles Enter, Space, Arrows, Escape
      className="dropdown"
    >
      <ul role="listbox">
        {items.map((item, i) => (
          <li
            key={item.id}
            role="option"
            tabIndex={-1}
            aria-selected={i === activeIndex}
            onKeyDown={e => e.key === 'Enter' && select(item)}
          >
            {item.label}
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Modals and dialogs

Modal dialogs must:

  1. Move focus into the dialog when it opens
  2. Trap focus inside while it's open (Tab cycles within)
  3. Return focus to the trigger element when it closes
function openModal(modalEl, triggerEl) {
  const focusable = modalEl.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last  = focusable[focusable.length - 1];

  first.focus();

  modalEl.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }
    if (e.key === 'Escape') closeModal(triggerEl);
  });
}
Enter fullscreen mode Exit fullscreen mode

Removing default focus styles

The single most common mistake: outline: none in a CSS reset.

/* ❌ Never do this globally */
* { outline: none; }

/* ✅ Remove default, replace with better style */
:focus { outline: none; }
:focus-visible {
  outline: 3px solid #0066CC;
  outline-offset: 2px;
  border-radius: 2px;
}
Enter fullscreen mode Exit fullscreen mode

The :focus-visible pseudo-class shows focus only when navigating by keyboard, not on mouse click — giving you the best of both worlds.

Skip Links

Users navigating by keyboard should be able to skip repetitive navigation. A skip link is the first focusable element in your page:

<a href="#main-content" class="skip-link">Skip to main content</a>
Enter fullscreen mode Exit fullscreen mode
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 9999;
}
.skip-link:focus { top: 0; }
Enter fullscreen mode Exit fullscreen mode

Automated Testing Coverage

Automated scanners can catch about 40% of keyboard accessibility issues — primarily missing tabindex, incorrect roles, and missing focus styles. Tools like AccessiScan provide a starting point with 201 automated checks, but the Tab-through test above is still essential for catching interaction patterns that automation misses.

Top comments (0)