Modal · a real <dialog>, not a div pretending to be one
Overlay
01
Detail drawer
A native <dialog>, opened with showModal(). Not a div with display:none, not a hand-rolled backdrop — the browser owns the stacking context, the focus trap, and the Escape key.
Dialog · detail drawer
showModal(), not display:none
Click the trigger. The browser handles the top-layer stacking, the focus trap inside the dialog, and Escape. None of that is hand-written.
On-brand: use this when a reader wants to go deeper on one thing without losing their place on the page — inspecting a component's real markup, expanding one spec entry. Not for anything the reader needs to compare against the page behind it.
02
Why native, not a div
Three things a hand-rolled modal gets wrong by default. The native element gets them free, or close to it.
// 1 — click-outside, computed against the dialog's own box. // Checking event.target === drawer is not enough: a click on // the dialog's own padding still targets the dialog element.
drawer.addEventListener('click', e => {
const box = drawer.getBoundingClientRect();
const inside = e.clientX >= box.left && e.clientX <= box.right
&& e.clientY >= box.top && e.clientY <= box.bottom;
if (!inside) drawer.close();
});
// 2 — Escape. No listener needed; native to <dialog>.
// 3 — focus return, on the dialog's own "close" event.
drawer.addEventListener('close', () => {
if (lastTrigger) lastTrigger.focus();
});
Correctness, not decoration
The bugs this avoids
A hand-rolled overlay usually gets one of these wrong first, and it's never obvious in a demo — only once a real reader tabs through it or clicks near an edge.
Backdrop click doesn't false-close on the dialog's own padding
Escape works without a keydown listener to maintain
Keyboard/screen-reader focus returns to the trigger, not the page top
Created for you, with ❤️ by Robert Evans· The AI Design Architect