Skip to content

Mark background content inert while a modal dialog is open - #299

Open
sagikazarmark wants to merge 1 commit into
DioxusLabs:mainfrom
sagikazarmark:inert
Open

Mark background content inert while a modal dialog is open#299
sagikazarmark wants to merge 1 commit into
DioxusLabs:mainfrom
sagikazarmark:inert

Conversation

@sagikazarmark

@sagikazarmark sagikazarmark commented Sep 2, 2026

Copy link
Copy Markdown

A modal DialogContent installs a focus trap that only intercepts Tab, so everything behind the dialog stayed reachable: by a pointer, by a programmatic focus() from application code, by a background element that autofocuses on mount, and — depending on the screen reader — by a browse mode cursor. The trap also binds to its own container, so once focus leaves by any non-Tab route no handler fires and it is never brought back. aria-modal="true" is set, but support for it has been inconsistent enough that the WAI-ARIA Authoring Practices and MDN both recommend pairing it with inert on background content.

While the trap is installed, walk from the dialog up to <body> and mark each ancestor's other children inert. Each dialog attributes its marks to an owner id in a data-inert-by marker, so inert the application had already set — which carries no marker — is left alone. The whole state is recomputed from the set of open dialogs on every open and close, which is what makes stacking compose: the dialog installed last is on top, so nothing on its path to <body> is marked, and every dialog underneath still marks its own background and so ends up inert itself. The unwind is addressed by owner id rather than by a recorded element list, so it also works when the dialog is unmounted while open.

The owner id comes from Rust rather than being derived from the element id, so that marking and unwinding cannot disagree about it, and so that a caller supplied id containing a space cannot corrupt the space separated marker.

Also fix focus restoration: FocusTrap.remove() focused the element that was active when the trap was built without checking it was still in the document. If the opener was unmounted while the dialog was open — routine when the dialog's action re-renders the view behind it — focus fell to <body> and the keyboard position was lost. It now falls back to the main landmark, made focusable only for as long as it holds focus.

DialogRoot and AlertDialogRoot take an inert_background prop (default true) for callers who manage inert themselves. The focus trap effect moves to a shared use_focus_trap hook so dialog and alert dialog compose by construction.

Fixes Status: #298

A modal `DialogContent` installs a focus trap that only intercepts Tab, so
everything behind the dialog stayed reachable: by a pointer, by a programmatic
`focus()` from application code, by a background element that autofocuses on
mount, and — depending on the screen reader — by a browse mode cursor. The trap
also binds to its own container, so once focus leaves by any non-Tab route no
handler fires and it is never brought back. `aria-modal="true"` is set, but
support for it has been inconsistent enough that the WAI-ARIA Authoring
Practices and MDN both recommend pairing it with `inert` on background content.

While the trap is installed, walk from the dialog up to `<body>` and mark each
ancestor's other children `inert`. Each dialog attributes its marks to an owner
id in a `data-inert-by` marker, so `inert` the application had already set —
which carries no marker — is left alone. The whole state is recomputed from the
set of open dialogs on every open and close, which is what makes stacking
compose: the dialog installed last is on top, so nothing on its path to `<body>`
is marked, and every dialog underneath still marks its own background and so
ends up inert itself. The unwind is addressed by owner id rather than by a
recorded element list, so it also works when the dialog is unmounted while open.

The owner id comes from Rust rather than being derived from the element id, so
that marking and unwinding cannot disagree about it, and so that a caller
supplied id containing a space cannot corrupt the space separated marker.

Also fix focus restoration: `FocusTrap.remove()` focused the element that was
active when the trap was built without checking it was still in the document. If
the opener was unmounted while the dialog was open — routine when the dialog's
action re-renders the view behind it — focus fell to `<body>` and the keyboard
position was lost. It now falls back to the main landmark, made focusable only
for as long as it holds focus.

`DialogRoot` and `AlertDialogRoot` take an `inert_background` prop (default
true) for callers who manage `inert` themselves. The focus trap effect moves to
a shared `use_focus_trap` hook so dialog and alert dialog compose by
construction.

Claude-Session: https://claude.ai/code/session_018qMUbzVT7pawv31DChxknW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant