Manage modal focus
Focus the dialog on open, close on Escape, and restore focus on close.
From frontend-patterns, “Accessibility Patterns: Focus Management”.
Where does focus go?
Tab to button 12, press Enter, then Tab and Escape.
Focus left behindBad
activeElement: —
outside: 0
Focus in, then backGood
activeElement: —
outside: 0
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement
modalRef.current?.focus()
} else {
previousFocusRef.current?.focus()
}
}, [isOpen])
return isOpen ? (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="rename-title"
tabIndex={-1}
onKeyDown={e => e.key === 'Escape' && onClose()}
>
{children}
</div>
) : null
}
What a screen reader hears
Tab to Delete, press Enter, then Escape.
A styled divBad
INV-2041Acme Co · $1,200.00
role="dialog", focusedGood
INV-2041Acme Co · $1,200.00
<div role="dialog" aria-modal="true" tabIndex={-1}
aria-labelledby="del-title" aria-describedby="del-desc">
<h3 id="del-title">Delete INV-2041?</h3>
<p id="del-desc">This can’t be undone.</p>
…
</div>
// focus() on open → "Delete INV-2041?, dialog, modal. This can’t be undone."
Why it matters, pros and cons
Why it matters
Focus is the keyboard user's cursor. A modal that drops it loses their place.
Pros
- Opening announces the dialog’s name and role.
- Escape closes, as every user expects.
- Restored focus keeps their place.
Cons
- Not a trap: Tab still leaves without
inert. - Native
<dialog>showModal()does all this free. - A deleted opener can’t take focus back.