When a modal dialog closes and focus drops to the document <body> or jumps to the page top, the keyboard user loses their place entirely. They must navigate from the beginning of the page to find where they were working. In a long page with complex navigation, this can require dozens of Tab presses. WCAG 2.2 SC 2.4.3 (Focus Order) requires that focus order preserve meaning and operability. SC 3.2.2 (On Input) guards against unexpected context changes. Section 508 2018 Refresh 302.1 covers programmatic determinability of primary functionality. The specific failure manifests in delete confirmation dialogs: user clicks Delete, dialog appears, user confirms, dialog closes — and focus is gone. The user cannot easily verify the deletion happened or continue their workflow.
Low because focus loss after dialog close is disorienting and adds navigation overhead but does not prevent users from accessing content with additional keystrokes.
Store a reference to the trigger element before opening the dialog, and explicitly call .focus() on it after the dialog closes. This is the responsibility of the component that controls the dialog's open state, not the dialog itself.
function UserCard({ userId }: { userId: string }) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const deleteButtonRef = useRef<HTMLButtonElement>(null);
const handleCloseDialog = () => {
setShowDeleteDialog(false);
// Return focus to the trigger synchronously after unmount
// Use requestAnimationFrame if using CSS transitions that delay unmount
requestAnimationFrame(() => {
deleteButtonRef.current?.focus();
});
};
return (
<div>
<h2>User Profile</h2>
<button
ref={deleteButtonRef}
onClick={() => setShowDeleteDialog(true)}
>
Delete Account
</button>
{showDeleteDialog && (
<ConfirmDialog
message="Delete this account permanently?"
onConfirm={async () => { await deleteUser(userId); handleCloseDialog(); }}
onCancel={handleCloseDialog}
/>
)}
</div>
);
}
If the trigger element is removed from the DOM after the action (e.g., deleting a list item), focus should move to the next logical element — the following list item or the list container.
ID: accessibility-basics.keyboard-focus.focus-restoration
Severity: low
What to look for: Enumerate every relevant item. When modals, dialogs, or overlay panels close, check whether focus returns to the element that triggered their opening. This prevents disoriented users and allows them to continue their workflow logically.
Pass criteria: At least 1 of the following conditions is met. When a modal or panel closes, focus returns to the trigger element that opened it.
Fail criteria: Focus does not return to the trigger element, or is lost entirely (sent to page top or body).
Skip (N/A) when: The application has no modal or panel components.
Detail on fail: Describe the focus restoration issue. Example: "When closing the dialog, focus is not returned to the trigger button. User is disoriented and cannot continue workflow."
Remediation: Store and restore focus on modal open/close:
const [isOpen, setIsOpen] = useState(false);
const triggerRef = useRef(null);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
// Restore focus to trigger
triggerRef.current?.focus();
};
return (
<>
<button ref={triggerRef} onClick={openModal}>Open Dialog</button>
{isOpen && <Modal onClose={closeModal} />}
</>
);