Keyboard-only users—including people with motor impairments, power users, and anyone using assistive switches—cannot interact with any element unreachable by Tab or that traps focus without escape. WCAG 2.2 SC 2.1.1 (Level A) and SC 2.4.3 (Level A) are both at stake, as is Section 508 2018 Refresh 503.3.3. A modal that swallows keyboard focus indefinitely, or a custom widget that responds only to mouse clicks, locks these users out entirely. Federal procurement standards mandate full keyboard operability, and failure here triggers compliance findings.
Critical because broken keyboard access completely prevents interaction for users who rely solely on keyboards or assistive input devices, violating WCAG 2.2 SC 2.1.1 at Level A.
Use semantic HTML (<button>, <a>, <input>) which receives keyboard focus by default. For custom widgets, add tabindex="0". Always preserve visible focus indicators—never suppress outline without a replacement:
button:focus-visible, a:focus-visible, input:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
For modal dialogs, trap focus inside while open and restore it to the trigger on close. In src/components/Modal.tsx, implement a focus-trap loop:
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstFocusable) {
e.preventDefault(); lastFocusable.focus();
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
e.preventDefault(); firstFocusable.focus();
}
}
if (e.key === 'Escape') closeModal();
});
gov-section-508.wcag-core.keyboard-navigationcriticaloutline: none without a replacement style). No keyboard traps exist. A partial or placeholder implementation does not count as pass. Report the count even on pass."Modal dialog does not trap focus; Tab moves focus outside the modal when last button is tabbed. Close button has outline: none with no replacement focus style, making it impossible to see where focus is on the button."<button>, <a>, <input>) which are keyboard-accessible by default. For custom interactive elements, add tabindex="0" to make them focusable. Always show focus indicators:
button:focus, a:focus, input:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Never remove focus outlines without a replacement style. For modal dialogs, trap focus by moving focus to the first focusable element when opened and preventing Tab from moving focus outside:
const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
});