Keyboard focus does not trap in any component
Why it matters
A focus trap in a modal or overlay that has no Escape key handler and prevents Tab from exiting leaves keyboard users permanently stuck — they cannot dismiss the overlay, access page content, or use browser navigation. WCAG 2.2 SC 2.1.2 (No Keyboard Trap) is a Level A requirement. Note: intentional focus trapping inside an open modal dialog is correct behavior (users should not Tab out of an open modal); the violation is when there is no way to close the dialog at all.
Severity rationale
Medium because an accidental focus trap prevents keyboard users from reaching any other part of the interface, but the violation is scoped to components that actively trap rather than the entire page.
Remediation
Ensure every modal and overlay can be dismissed with the Escape key, and that closing it returns focus to the trigger element.
export function Modal({ open, onClose, triggerRef }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!open) return
// Focus first focusable element inside modal
const first = modalRef.current?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
first?.focus()
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handleKey)
return () => document.removeEventListener('keydown', handleKey)
}, [open, onClose])
// Restore focus to trigger on close
useEffect(() => {
if (!open) triggerRef.current?.focus()
}, [open])
return open ? <div ref={modalRef} role="dialog" aria-modal="true">{/* content */}</div> : null
}
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Accessibility WCAG Compliance Audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- wcag:2.2 · 2.1.2 — No Keyboard Trap
- section-508:2018-refresh · 501.1 — Scope
Taxons
History
- 2026-04-18·v1.0.0·Initial import from accessibility-wcag·automated