Skip to content

FocusScope skips a Tab stop after an exiting sibling scope unmounts #10593

Open

Description

@randomspells

🐛 Bug Report

When one overlay opens another before it finishes closing, unmounting the first overlay can break keyboard navigation in the second. One Tab skips a focusable element. On a fresh opening, Shift+Tab can move focus outside the second overlay even though its FocusScope has contain enabled.

The two scopes are siblings under an outer FocusScope in the React component tree. The second scope stays mounted, and focus is already inside it when the first scope unmounts.

🤔 Expected Behavior?

Removing the closing overlay should not change keyboard navigation in the overlay that remains open. Tab should move to the next focusable element, and Shift+Tab from the first element should wrap to the last. Focus should stay within the containing scope.

😯 Current Behavior

After the closing overlay unmounts, the first Tab skips the next button. Testing Shift+Tab instead on a fresh opening moves focus to a button outside the remaining overlay.

💁 Possible Solution

Change FocusScope cleanup so that it resets activeScope only when the scope being removed is itself active. If the active scope is a descendant that remains mounted, preserve it while removing its ancestor and reparenting it.

In the cleanup condition, this means replacing scopeRef === activeScope || isAncestorScope(scopeRef, activeScope) with scopeRef === activeScope. The isAncestorScope helper remains in its other call sites.

The added forward and reverse Tab regression tests fail without this change and pass with it. The existing whole-subtree focus restoration test also passes unchanged.

🔦 Context

Selecting a menu item opens another popover. The menu briefly remains mounted while the new popover opens. Once the menu unmounts, the first Tab in the new popover skips a button.

🖥️ Steps to Reproduce

https://codesandbox.io/p/sandbox/xfgnvy

  1. Open the standalone preview.
  2. Focus Open menu and press Space. Wait for focus to reach Choose date and time.
  3. Press Space again, then wait for the first overlay to fade out. Focus is now on September, the first enabled button in the second overlay.
  4. Press Tab once. Expected: 2026. Actual: Next month, skipping 2026.
  5. Reload and repeat steps 2–3. Press Shift+Tab instead. Expected: Next month (wrap to the last button). Actual: Open menu, outside the second overlay.

After the closing overlay unmounts, a single Tab skips a button in the overlay that remains open, even though its FocusScope still has contain enabled.

Version

react-aria@3.52.1

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

macOS 26.4.1, arm64

🧢 Your Company/Team

No response

🕷 Tracking Issue

N/A

Activity

  1. dvd233 commented on Sep 10, 2026

    @dvd233

    Thanks for the detailed reproduction. I’m going to work on this by adding forward and reverse Tab regression coverage for the surviving sibling scope, and by limiting the cleanup reset to the scope that is actually active. I’ll also keep the existing whole-subtree unmount coverage in the focused test run.

  2. snowystinger commented on Sep 12, 2026

    @snowystinger
    Member

    Is the codesandbox representative of how you've actually built this? Or are you using RAC Modal/Popover? I'm asking because the FocusScope used by those components applies some extra logic when they are exiting

    <FocusScope restoreFocus contain={(props.shouldContainFocus || contain) && !isExiting}>

  3. randomspells commented on Sep 14, 2026

    @randomspells
    Author

    Is the codesandbox representative of how you've actually built this? Or are you using RAC Modal/Popover? I'm asking because the FocusScope used by those components applies some extra logic when they are exiting

    <FocusScope restoreFocus contain={(props.shouldContainFocus || contain) && !isExiting}>

    Hi! The application uses custom components (modal and popover) built on FocusScope. The reproduction uses simplified versions of those components, with contain set to true while open and false when closing.

  4. snowystinger commented on Sep 15, 2026

    @snowystinger
    Member

    If you stop containing during the animation out, does it work as expected?

  5. randomspells commented on Sep 15, 2026

    @randomspells
    Author

    If you stop containing during the animation out, does it work as expected?

    No, the menu already has contain={false} while it fades out, but Tab skips a button after the menu disappears.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions