[Bug Fix] Dialog: play the exit animation before closing the native <dialog>

TV
1mo ago
+207-34

Closing a Dialog cut the panel and its backdrop off in the same frame: dialog.close() drops open (and with it display) immediately, and Escape went through the native path untouched. Every other overlay animates out since #506; Dialog now does too.

  • DialogContent keys its animations on data-state (set by the controller) instead of the open: variant, so the closed state can still render the exit. open:flex stays — a closed dialog remains hidden.
  • The backdrop gets backdrop:duration-200 so it fades in step with the panel (--tw-duration is registered inherits: false, so ::backdrop does not pick the panel's duration up). The controller does not depend on the two matching.
  • Controller: dismiss() sets data-state="closed", waits for the panel's and the backdrop's exit animations (getAnimations({subtree: true}) on the <dialog> filtered to its own effect.target, their finished promises, a token per run so a superseded run cannot settle a later one), then calls close(). This differs from the animationend block in the other overlays because the ::backdrop's events land on the <dialog> under the same keyframe names. cancel (Escape, requestClose()) is routed through dismiss(); a second one mid-exit is left to the browser (non-cancelable in Chrome anyway). A close the controller did not start settles the pending run. Backdrop click, focus restore and body scroll lock are unchanged. closedby="any" is not used: it would double-fire with the click handler and Safari support is recent.
  • Teardown and scroll lock follow #518/#520: disconnect() guards hasDialogTarget (a <dialog> removed before its wrapper no longer throws and the lock still lifts), and the body class is released only when no dialog:modal is left in the document, so a Dialog closed or removed on top of another modal keeps the page locked for the one underneath.
  • Tests for the class contract; MCP registry rebuilt. Markup and usage unchanged.

AlertDialog and Combobox get the same treatment in separate PRs.

Test: /docs/dialog, each example — close via Ɨ, Cancel, Escape and a backdrop click: panel and backdrop fade out (~200 ms) before the dialog disappears, focus returns to the trigger, page scroll is restored. Click inside stays open; reopen mid-exit comes back; open/close quickly still plays the full exit; Escape twice closes on the second press and the next cycle animates normally; a duration-* override on DialogContent closes after the longer of panel and backdrop. Open a second Dialog on top of the first and close or remove it: the page stays locked until the first one closes.

šŸ¤– Generated with Claude Code


Summary by cubic

Fixes Dialog closing so the panel and backdrop animate out before the native <dialog> closes, and adds a show_close_button option to DialogContent.

Bug Fixes

  • Animations key on data-state instead of the open: variant, so the closed state can render the exit; open:flex still keeps a closed dialog hidden.
  • The backdrop shares the panel's duration via backdrop:duration-200.
  • dismiss() sets data-state="closed", waits for the panel and backdrop exit animations to finish, then calls close(); Escape and requestClose() route through this path.
  • Reopening mid-exit cancels the pending run, and a second Escape mid-exit closes natively where the browser allows it.
  • Teardown and scroll-lock release are guarded: removing a dialog no longer throws, and the lock stays while another modal dialog (nested Dialog, Sheet) is still open.

New Features

  • DialogContent accepts show_close_button: false to hide the corner close button.
  • Added a "No close button" docs example; docs triggers now use Button(variant: :outline) to match the Sheet and Drawer docs.