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.
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.
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
}
ID: accessibility-wcag.operable.focus-trap
Severity: medium
What to look for: Enumerate every relevant item. Keyboard-navigate through modals, dropdowns, and overlays. Verify that Tab moves focus out of the component, and Escape key closes overlays (if applicable).
Pass criteria: At least 1 of the following conditions is met. Keyboard focus can move freely. No components trap focus. Tab and Shift+Tab allow navigation into and out of all components. Modals have an Escape key handler to close. Applications with no modal or overlay components pass by default.
Fail criteria: Focus is trapped within a component with no way to escape via Tab key or Escape. An accidental focus trap (e.g., event listener that prevents Tab propagation) causing keyboard users to be stuck.
Skip (N/A) when: No interactive components exist in the application at all (fully static page with no focusable elements).
Detail on fail: Example: "Modal dialog traps focus — tabbing loops within modal, cannot return to page content. Escape key does not close modal"
Remediation: For modals, manage focus with a focus trap library or manually:
import { useEffect } from 'react'
export function Modal({ open, onClose }) {
const modalRef = useRef(null)
useEffect(() => {
if (!open) return
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [open, onClose])
return open ? <div ref={modalRef} role="dialog">{/* content */}</div> : null
}