Fixing Focus That Doesn't Return to the Triggering Element After a Modal Closes
Opening a modal and then closing it with Escape or a Cancel button leaves a keyboard user's focus sitting nowhere meaningful — not back on the button that opened it, sometimes not anywhere identifiable at all. Trapping focus correctly while the modal is open solves half the problem; the other half is a deliberate, separate step that's easy to skip entirely.
The Problem
A modal dialog correctly traps keyboard focus while it's open — Tab cycles through the dialog's own interactive elements without escaping to the page behind it, which is the fix for a different, already well-known accessibility bug. But the moment the modal closes, whether by pressing Escape, clicking a Cancel button, or submitting a form inside it, a keyboard user's focus doesn't land back on whatever triggered the modal in the first place. Depending on the browser and the specific markup, focus can end up on the <body> element, effectively nowhere a screen reader or sighted keyboard user can make sense of, or stuck on an element that no longer exists in the DOM.
Why It Happens
Removing the modal from the DOM doesn't automatically move focus anywhere specific
When a modal unmounts or becomes hidden, whatever element inside it currently had focus is, from the browser's perspective, simply gone — there's no built-in behavior that moves focus to "wherever it was before the modal opened." Unless something in the application code explicitly captures and restores that prior focus position, the browser's own default handling of a removed focused element (commonly falling back to the document body) is what a keyboard user actually experiences.
The reference to the triggering element was never actually saved, or was saved at the wrong moment
Correctly restoring focus requires knowing, at the moment the modal opens, exactly which element currently had focus — so that same element can be refocused later. A modal implementation that only concerns itself with trapping focus while open, without capturing document.activeElement at the point of opening, has nothing to restore focus to later, regardless of how well the trap itself works.
The triggering element can be gone, disabled, or re-rendered into a different DOM node by the time the modal closes
Even when a reference to the triggering element was captured correctly, that element might not exist anymore by the time the modal closes — a list item that opened a confirmation modal and was then deleted, a button that got disabled during the async operation the modal was confirming, or a framework re-render that replaced the element with a structurally identical but distinct DOM node the saved reference no longer points to. Attempting to call .focus() on a reference that's stale in any of these ways either throws, silently does nothing, or sends focus somewhere unintended.
The Fix
1. Capture the currently focused element the instant the modal opens, before moving focus into the dialog
function openModal() {
previouslyFocusedElement.current = document.activeElement;
setIsModalOpen(true);
// focus then moves into the modal's own first focusable element
}
Saving document.activeElement at the exact moment the modal is triggered — before anything else changes focus — captures the one piece of information needed to restore it correctly later, which is the prerequisite every other fix in this list depends on.
2. Explicitly refocus the saved element when the modal closes, rather than relying on default browser behavior
function closeModal() {
setIsModalOpen(false);
previouslyFocusedElement.current?.focus();
}
Calling .focus() on the saved reference as a deliberate step in the close handler — rather than assuming the browser will figure out where focus should go — is the direct fix for the core problem, since nothing about removing or hiding the modal does this automatically on its own.
3. Guard the restore call against a stale or removed reference before calling focus on it
function closeModal() {
setIsModalOpen(false);
const el = previouslyFocusedElement.current;
if (el && document.contains(el) && !el.disabled) {
el.focus();
} else {
// fall back to a stable, known-safe target, e.g. the main content heading
mainHeadingRef.current?.focus();
}
}
Checking that the saved element still exists in the document and isn't disabled before focusing it handles the case where the triggering element was deleted, disabled, or replaced while the modal was open, falling back to a deliberately chosen, always-available target instead of a .focus() call that silently fails or does nothing useful.
4. Verify the fix by testing the specific flow where the triggering element changes while the modal is open
Open a confirmation modal from a delete button, confirm the deletion
(which removes that button's row from the list), and check specifically
where keyboard focus lands once the modal closes — not just the simple
open-then-cancel case, which can pass even when the fallback logic is missing.
Testing only the straightforward open-and-cancel flow can pass even without the stale-reference guard in place, since the triggering element is still there in that case; deliberately testing the flow where the action inside the modal removes or changes the trigger is what actually exercises the fallback path and confirms it works.
Why This Works
Each step addresses a link in the same chain: restoring focus correctly requires knowing where it came from, deliberately sending it back there, handling the case where "there" no longer safely exists, and testing specifically for the scenario where that gap would otherwise go unnoticed. Capturing the reference on open is the prerequisite all restoration depends on. Explicitly refocusing on close is the direct fix for the core gap in default browser behavior. Guarding against a stale reference prevents the fix itself from failing silently in a common real-world case. And testing the trigger-removed scenario specifically is what catches a fix that looks complete in the simple case but isn't.
Conclusion
Focus trapping correctly inside an open modal only solves half of the accessibility requirement — the other half is deliberately restoring focus to a sensible place once the modal closes, which nothing in default browser behavior does automatically. Capture the triggering element's reference the moment the modal opens, explicitly call .focus() on it in the close handler, guard that call against a reference that's since become stale or disabled, and test specifically the case where the modal's own action removes or changes the element that originally triggered it.