Pure server-driven <dialog> demo

Approach: The server returns a fully-formed <dialog> element. htmx swaps it into the DOM via hx-swap="outerHTML". A 7-line shim calls showModal() on any <dialog data-modal> that arrives. All close interactions use native <form method="dialog"> — zero JS.

Future state: When command="show-modal" reaches ~95% adoption, delete dialog-shim.js and add commandfor/command="show-modal" to the trigger buttons. The rest stays identical.

Modal

First open: hx-target="body" hx-swap="beforeend" — appends dialog to body.
Subsequent: hx-target="#drupal-dialog" hx-swap="outerHTML" — replaces in place.

Close on backdrop — closedby="any"

Native Chrome 133+, Firefox 136+. Shim polyfills for Safari via a delegated click listener.

Non-modal

Server sets open attribute directly. No shim involvement.

Persist (stays in DOM after close)

Server sets data-persist on the <dialog>. Close removes it from view but not the DOM — reopen with no fetch.

Replace content in open dialog

Target the existing #drupal-dialog with hx-swap="outerHTML". Server returns a new <dialog data-modal> — shim calls showModal() again.

Server-initiated close

Server returns a fragment that removes #drupal-dialog from the DOM.

ARIA — role="alertdialog"

Server sets role, aria-labelledby, aria-describedby directly on <dialog>.

Future state — command="show-modal"

No shim. Chrome 135+ only (~65% today). Shows what the zero-JS future looks like.

Wire log