Use the native dialog element: you get more than you think
A hand-rolled modal needs a focus trap, ESC handling, scroll locking and focus restore. Native dialog with showModal has all of it plus ::backdrop, and one style rule as the price.
For a modal, my default has changed: a native <dialog> with showModal() instead of a <div class="overlay">.
Four things showModal gives you
showModal() is not the same as the open attribute. It puts the element in the top layer, and four things you would otherwise write become built-in behaviour:
- A focus trap: Tab cannot reach the page behind it, and the background is
inertfor assistive technology - ESC to close: no
keydownlistener of your own, the closing event isclose - Scroll locking: the document behind it stops scrolling, with no
overflow: hiddenonbody - Focus restore: on close, focus returns to the element that opened it, remembered by the browser
Top layer rendering also settles z-index for free. The modal is above everything, no ancestor with overflow: hidden can clip it, and the header needs no larger z-index to stay out of the way.
::backdrop is the one style trap
The scrim is not the background of the dialog. It is a pseudo-element with the same name:
dialog::backdrop {
background: color-mix(in srgb, var(--c-ink) 60%, transparent);
}
It does not inherit custom properties from the dialog, because ::backdrop is its own box in the top layer. If you want it to read site tokens, declare them on :root rather than on the dialog. This is the part that confuses people most.
When a hand-rolled layer is still right
The tool pages here have a pure CSS ui-overlay (result panels, loading hints). That is a display layer, not a modal: nothing to trap, no interaction, and scrolling away is fine. Real modals, like confirming a destructive action, use dialog.
Reach for the native element first. Visual design should be the only remaining reason to build your own.

Comments
…