Modal Popover: focus restoration is dropped when the restore frame beats the inert cleanup

Author: alexandru-bereghici-toCreated Sep 16, 2026Updated Sep 17, 2026

Feature Request / Bug Report

A modal Popover can close without restoring focus: FocusScope restores from a requestAnimationFrame scheduled in a layout-effect cleanup, while usePopover takes the inert off the rest of the page in a passive-effect cleanup. React runs layout-effect cleanups during the commit and flushes passive-effect cleanups afterwards, so when the animation frame arrives before that flush, restoreFocusToElement calls focus() on a trigger that is still inert. Per the HTML focusing steps — "If new focus target is a focusable area and its DOM anchor is inert, then return" (https://html.spec.whatwg.org/multipage/interaction.html#focusing-steps) — the call does nothing and reports nothing. Nothing retries, so focus stays on <body>.

It fails when the frame is fast, not when it is slow: on a loaded machine the frame lands 0.5–1.5 ms after the unmount instead of the 5–7 ms that lets the passive flush go first.

The code path (main @ 4693fcc844a341107e7dd26fe456edb1e269e44c)

Nothing orders the two, and focusElement does not check that the focus landed.

Expected Behavior

Closing a modal popover returns focus to the trigger.

Current Behavior

document.activeElement is <body>, and stays there. Instrumented output from a failing run, recorded by patching HTMLElement.prototype.focus to log its stack and, when the call did not move document.activeElement, the element's connectivity, computed style and every ancestor carrying inert, aria-hidden, hidden, display:none or visibility:hidden:

22.4 raf#4 fired after 7.9ms
22.4 raf#5 fired after 7.9ms
22.4 raf#8 fired after 0.5ms
22.6 focus() on button#react-aria-_r_1t_ "Delete" :: … | focusElement | restoreFocusToElement
22.6   !! focus did not land. active=body hasFocus=true connected=true tabindex=0
       disabled=false display=inline-flex visibility=visible pointerEvents=auto
       blockers=[button#react-aria-_r_1t_ "Delete"{inert}] rootNode=document
1040.5 end active=body

restoreFocusToElement picks the right element; the browser drops the call because the element is still inert.

Possible Solution

Anything that orders the two, or that notices the drop:

  • clear the inert in a layout-effect cleanup, so it is gone before any restore frame runs; or
  • have restoreFocusToElement check ownerDocument.activeElement after focusElement and retry once the inert is cleared.

Context

In a design-system suite of ~1,070 browser stories run in Chromium, this failed 11 times in 36 runs (~31%) with test retries disabled — every failure a modal popover closing. It is masked by a retry, which is why it reads as flake rather than as a defect. We currently repair it in our own overlay host by listening for the react-aria-focus-scope-restore event, and re-focusing its target one frame after close if document.activeElement is <body>.

Code Sample

javascript
<DialogTrigger>
  <Button>Delete</Button>
  <Popover>
    <Dialog>
      <Button slot="close">Cancel</Button>
    </Dialog>
  </Popover>
</DialogTrigger>

Open it and press Escape. Under main-thread load the restore frame beats React's passive flush and focus is left on <body>. What the restore does in that interleaving, on any browser:

javascript
trigger.inert = true;
trigger.focus();
document.activeElement === document.body; // true — the focusing steps return early

Your Environment

Software Version(s)
react-aria 3.51.0; 3.52.1 has the same race — usePopover.mjs and ariaHideOutside.mjs are byte-identical between the two, and FocusScope.mjs differs only by a null guard
react-aria-components 1.20.0
React 19.2.7
Browser Chromium (Playwright 1.61.1)
Operating System macOS 15 (Darwin 25.6.0)