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 inert for assistive technology
  • ESC to close: no keydown listener of your own, the closing event is close
  • Scroll locking: the document behind it stops scrolling, with no overflow: hidden on body
  • 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.

← Back to all posts

Comments

…