Modals trap focus and communicate their purpose
Why it matters
A modal dialog that does not trap focus allows Tab presses to reach background content — visually hidden behind the overlay — while screen reader users may never realize the modal is open at all. WCAG 2.2 SC 2.1.2 (No Keyboard Trap) is often misread as 'don't trap focus,' but the actual requirement is that users can both enter and exit a trap using standard keys (Escape closes the modal). SC 2.4.3 (Focus Order) and SC 4.1.2 (Name, Role, Value) require modals to be correctly marked up with role="dialog" and aria-modal="true". Section 508 2018 Refresh 502.3.1 covers programmatic determinability of role. When aria-modal is absent, some screen readers ignore the visual overlay entirely and continue reading background content as if the modal weren't there, leading users to interact with controls they cannot see.
Severity rationale
Medium because focus-trap failures create confusing and disorienting experiences for keyboard users without blocking all access to the application.
Remediation
Use a battle-tested focus trap library (focus-trap-react, @radix-ui/react-dialog) rather than implementing from scratch. If implementing manually, ensure focus moves in, Tab cycles only within the modal, Escape closes it, and focus returns to the trigger on close.
import { useEffect, useRef } from 'react';
export function Modal({ isOpen, onClose, title, children, triggerRef }) {
const firstFocusRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) {
// Move focus into modal immediately
firstFocusRef.current?.focus();
} else {
// Return focus to trigger on close
triggerRef.current?.focus();
}
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleKey);
return () => document.removeEventListener('keydown', handleKey);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<h2 id="modal-title">{title}</h2>
{children}
<button ref={firstFocusRef} onClick={onClose}>Close</button>
</div>
);
}
For most applications, reach for @radix-ui/react-dialog which handles all of this correctly by default.
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 Basics 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
- wcag:2.2 · 4.1.2 — Name, Role, Value
- wcag:2.2 · 2.4.3 — Focus Order
- section-508:2018-refresh · 502.3.1 — Object Information
Taxons
History
- 2026-04-18·v1.0.0·Initial import from accessibility-basics·automated