#4143·primitives

DismissableLayer 1.1.19: Escape during layer registration dismisses the layer underneath (stacked Dialogs both close)

Author: adoooo1111-opsCreated Sep 13, 2026Updated Sep 13, 2026

Packages: @radix-ui/react-dismissable-layer 1.1.19 (via @radix-ui/react-dialog 1.1.23). Not reproducible with react-dismissable-layer 1.1.11 / react-dialog 1.1.15.

Describe the bug

With two stacked modal Dialogs (A open, B opened from A), pressing Escape while B is still registering its DismissableLayer closes both A and B. With the older versions, an Escape in that window is ignored entirely (neither dialog closes); B closes only once its registration has propagated. Either way, A is never dismissed.

What changed upstream

PR #3968 (commit 37f522f2) moved the "is this the highest layer" check from the keydown handler into render. There is a window after B's layer has been added to the layer set but before A's effect has re-run: A's useEscapeKeydown listener is still attached and still considers itself the top layer, so an Escape in that window dismisses A as well as B. Because the listeners are attached on document in the capture phase, an app-level handler on the dialog content cannot intercept it.

Reproduction (jsdom / Testing Library, deterministic)

typescript
// Two modal Dialogs; the second one is opened from a button inside the first.
// Listen for Radix's internal 'dismissableLayer.update' event: it fires when B's
// layer is added to the set. Dispatch Escape right then.
document.addEventListener('dismissableLayer.update', () => {
  document.querySelector('#input-in-dialog-b')
    ?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }));
}, { once: true });
fireEvent.click(screen.getByRole('button', { name: 'Open B' }));
// expected: Dialog A still open (B either still open, as in 1.1.11, or closed)
// actual (1.1.19): both closed — onOpenChange(false) fires on A

Also reproducible in a browser (Playwright, 344 px viewport) with a fast open-then-Escape sequence.

Expected behavior

Escape dismisses only the top-most layer, regardless of whether the new layer's registration has fully propagated.

Suggested fix

Guard the escape handler with the layer set at event time (as before #3968), or defer attaching the new layer's keydown listener until the previous top layer has re-rendered with the updated index.