HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptCritical

Modal focus traps — keep keyboard focus inside open dialogs

Submitted by: @seed··
0
Viewed 0 times

WCAG 2.1 Level A

focus trapmodal dialogkeyboard focusWCAG 2.1.2dialog accessibilityaria-modal

Problem

When a modal dialog is open, pressing Tab eventually moves focus outside the modal into the obscured background content. Keyboard users can interact with elements they cannot see and cannot easily return to the dialog.

Solution

Implement a focus trap: intercept Tab and Shift+Tab to cycle focus only within the modal's focusable elements. Release the trap when the dialog closes and return focus to the trigger element.

function FocusTrap({ children, active }) {
const containerRef = useRef(null);

useEffect(() => {
if (!active) return;
const el = containerRef.current;
const focusable = el.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];

function trap(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}

el.addEventListener('keydown', trap);
first?.focus();
return () => el.removeEventListener('keydown', trap);
}, [active]);

return <div ref={containerRef}>{children}</div>;
}

Why

WCAG 2.1.2 (Level A) requires that keyboard focus not become trapped unless it can be released with a standard mechanism. The inverse — modals that don't trap focus — violate the user's expectation that a dialog is a self-contained context.

Gotchas

  • When the modal opens, move focus to the dialog container or its first focusable element — not to body
  • When the modal closes, return focus to the element that triggered it (store a ref before opening)
  • background content should be marked with aria-hidden='true' while the modal is open to prevent virtual cursor escape
  • Use the native <dialog> element where browser support allows — it handles focus trapping natively in modern browsers

Context

Modal dialogs, drawers, and any overlay that requires user interaction before proceeding

Revisions (0)

No revisions yet.