The accessible-modal checklist is long and unforgiving: trap focus inside, restore focus on close, handle Escape, mark the background inert, stack above everything, announce correctly to screen readers. Every hand-rolled modal ships bugs against that list—usually the focus ones, which only keyboard and screen-reader users hit.

<dialog> ships the checklist in the browser. One element, one method:

<dialog id="confirm" closedby="any">
  <h2>Delete this post?</h2>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Delete</button>
  </form>
</dialog>

// modal mode: focus trap + inert page + top layer
confirm.showModal();

showModal() is the important call: it puts the dialog in the top layer, makes the rest of the page genuinely inert, traps Tab, and wires Escape. closedby="any" adds light-dismiss—click the backdrop, dialog closes—without a click-outside listener. And method="dialog" forms close the dialog and report which button was pressed via returnValue, no handlers required.

“Every hand-rolled focus trap is a bug report from a keyboard user you have not met yet.”

The styling story

::backdrop styles the scrim directly—blur it, tint it, animate it. Entry animations use the same @starting-style pattern as popovers, so dialogs can scale-and-fade in with pure CSS. The decision tree is short now: interruptive flows get dialog.showModal(); passive overlays get popover; and the modal library gets deleted.