Summary
<Dialog> announces itself as role="dialog" aria-modal="true" but implements none of the behaviour that contract implies. Built from the library's own parts, a dialog opens and then cannot be closed by any of the three ways a user will try, and the page behind it is still fully interactive.
Reproduction
Exactly the composition the component README implies:
<Dialog :open="dlgOpen()" @close="dlgOpen.set(false)">
<DialogBackdrop />
<DialogPanel>
<DialogTitle>Delete this workout?</DialogTitle>
<DialogDescription>This cannot be undone.</DialogDescription>
</DialogPanel>
</Dialog>Open it, then measure:
| Behaviour | Result |
|---|---|
| Click the backdrop | dialog stays open |
| Press Esc | dialog stays open |
document.body scroll lock | overflow: visible — the page scrolls behind the modal |
| Focus moved into the dialog | false — focus stays on the trigger button |
| Focus trap | none — Tab walks straight out into the page behind |
There is no built-in close affordance either, so a dialog whose author does not supply their own Cancel button is a dead end.
Why Esc does not work
Dialog.stx binds the handler to its own root element:
<div :show="isOpen()" role="dialog" aria-modal="true" @keydown="onBackdropKey($event)">Since focus is never moved into that element when the dialog opens, the keydown never reaches it. A synthetic keydown dispatched on the dialog node does close it — so the handler is correct and simply unreachable. The two defects compound: fixing focus would also fix Esc.
Why backdrop click does not work
DialogBackdrop.stx is aria-hidden="true" with no handler, and Dialog does not listen for clicks outside its panel. Nothing is wired to the dismiss path.
(The backdrop is also fully opaque — that is #1977, filed separately.)
Prior art in this same package
Drawer.stx gets all of this right: it locks body scroll (overflow: hidden measured), moves focus into the panel, scopes the focus lookup to its own $uid to survive two drawers on a page, and renders a working close button. Dialog reads like the version of Drawer from before that work landed.
Expected
Dialog should, on open: move focus into the panel, trap Tab within it, lock body scroll, restore focus to the trigger on close, and close on Esc and on backdrop click — with an opt-out for the latter two, since a destructive confirm sometimes wants to insist.
Until then, an app cannot use <Dialog> for a modal without reimplementing the modal part, which is most of it.
Environment
@stacksjs/components0.2.339, stacks 0.75.31, Bun 1.3.x, macOS- Found while migrating hq.training onto the component library — 21 hand-rolled modals there, all of which currently hand-roll backdrop dismissal because the component does not provide it.