[Bug Fix] Dialog: play the exit animation before closing the native <dialog>
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.
DialogContentkeys its animations ondata-state(set by the controller) instead of theopen:variant, so the closed state can still render the exit.open:flexstays ā a closed dialog remains hidden.- The backdrop gets
backdrop:duration-200so it fades in step with the panel (--tw-durationis registeredinherits: false, so::backdropdoes not pick the panel's duration up). The controller does not depend on the two matching. - Controller:
dismiss()setsdata-state="closed", waits for the panel's and the backdrop's exit animations (getAnimations({subtree: true})on the<dialog>filtered to its owneffect.target, theirfinishedpromises, a token per run so a superseded run cannot settle a later one), then callsclose(). This differs from theanimationendblock in the other overlays because the::backdrop's events land on the<dialog>under the same keyframe names.cancel(Escape,requestClose()) is routed throughdismiss(); 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()guardshasDialogTarget(a<dialog>removed before its wrapper no longer throws and the lock still lifts), and the body class is released only when nodialog:modalis 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-stateinstead of theopen:variant, so the closed state can render the exit;open:flexstill keeps a closed dialog hidden. - The backdrop shares the panel's duration via
backdrop:duration-200. dismiss()setsdata-state="closed", waits for the panel and backdrop exit animations to finish, then callsclose(); Escape andrequestClose()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
DialogContentacceptsshow_close_button: falseto 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.