Skip to content

Allow customising focus restoration target when ModalOverlay trigger element is removed from DOM #9876

@BlakeSearlSonocent

Description

@BlakeSearlSonocent

Provide a general summary of the feature here

Allow customising or disabling focus restoration when a ModalOverlay closes, so consumers can control where focus lands when the original trigger element has been removed from the DOM.

Perhaps there are already patterns to handle this but I could not find any!

🤔 Expected Behavior?

When a ModalOverlay closes and the element that was focused before it opened is no longer in the DOM, there should be a way to specify a fallback focus target or opt out of automatic restoration. This would prevent focus from falling to document.body.

😯 Current Behavior

ModalOverlay uses useModalOverlay which sets up a FocusScope with restoreFocus hardcoded to true. When the modal closes, FocusScope tries to restore focus to the previously focused element. If that element has been removed from the DOM, focus falls to document.body.

There is no prop on ModalOverlay or Modal to disable or redirect this behaviour.

This causes a visible focus flicker when consumers try to redirect focus using deferred calls (setTimeout(0) or requestAnimationFrame), since there is always at least one frame where focus sits on body before the deferred call runs. For screen reader users, this also leads to a brief and interrupted announcement of the document.body element.

💁 Possible Solution

Some potential options:

  • Expose restoreFocus as a prop on ModalOverlay. Allowing false would let consumers opt out and manage focus themselves without racing against the default behaviour.
  • restoreFocusTo prop — an element id (or ref but preference for id) that overrides the default restoration target.
  • Fallback behaviour — when the original trigger element is no longer in the DOM, restore focus to an element specified by id instead of letting it fall to body.

🔦 Context

We have a confirmation dialog(s) where the triggering button is replaced by a loading state after the user confirms. We need focus to move to a parent container that displays the loading status. Our workaround of using setTimeout(0) in the onClose callback works functionally, but causes a visible single-frame flicker as focus briefly sits at body between FocusScope restoration and our deferred focus call.

Workarounds tried:

  • setTimeout(0) in onClose — works but causes flicker
  • requestAnimationFrame — same flicker
  • useEffect cleanup watching the open prop — same timing issue
  • Focusing before the dialog closes — blocked by the modal's focus trap

Related issues:

💻 Examples

// Option 1: disable restoreFocus
<ModalOverlay isOpen={open} restoreFocus={false}>

// Option 2: specify a fallback target by id
<ModalOverlay isOpen={open} restoreFocusTo="outline-panel">

// Option 3: callback for custom handling
<ModalOverlay isOpen={open} onRestoreFocus={(originalElement) => {
  if (document.contains(originalElement)) {
    originalElement.focus()
  } else {
    document.getElementById('other-element')?.focus()
  }
}}>

🧢 Your Company/Team

Genio (note-taking and presentation applications)

🕷 Tracking Issue

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions