<drupal-dialog> custom element demo

Approach: <drupal-dialog> is a custom element (~30 lines, no dependencies) that owns showModal(), closedby polyfill, and self-removal on close. The server delivers two <hx-partial> tags per response — delete the old slot by id, insert the new one before end of body. No htmx extension, no init code, no container div. Trigger buttons carry only hx-action.

Slots: #drupal-modal (modal) and #drupal-off-canvas (non-modal) are independent — both can be open simultaneously. Custom slots use any id.

Modal

Close on backdrop — closedby="any"

Native Chrome 133+, Firefox 136+. <drupal-dialog> polyfills for Safari.

outerMorph — form validation in open tray

Open the tray, click a block, then submit the form empty. The server returns the same <drupal-dialog id="drupal-off-canvas"> with validation errors via hx-target="#drupal-off-canvas" hx-swap="outerMorph". The tray stays open, focus returns to the invalid field, scroll position is preserved. No delete+insert — morph updates the existing element in place.

Off-canvas tray — Layout Builder pattern

The tray uses #drupal-off-canvas. Clicking a block inside sends a new request that delete+inserts the same slot — the tray content is replaced but it stays open. No persist needed. Open a modal on top to see both slots coexist.

Non-modal — #drupal-off-canvas slot

Independent slot from #drupal-modal. Open both simultaneously to see Layout Builder-style coexistence.

Persist (stays in DOM after close)

data-persist on <drupal-dialog> — close hides it, DOM is kept. Two variants: inserted open (opens immediately), inserted closed (stays hidden until commandfor triggers it).

Note: commandfor targets the inner <dialog id="persist-dialog-inner">, not the <drupal-dialog id="persist-dialog"> wrapper — two ids are required.

Replace — non-modal replaced by modal

Open a non-modal first so the page stays interactive. The second response targets the same #drupal-modal slot — delete partial removes it, insert partial appends a modal replacement. connectedCallback calls showModal() on the new one.

Server-initiated close

Open a non-modal first so the page stays interactive. Server response contains only <hx-partial hx-target="#drupal-modal" hx-swap="delete">. disconnectedCallback fires on the custom element for cleanup. Or return a new closed dialog — stays in DOM, commandfor reopens it.

Note: commandfor targets the inner <dialog id="drupal-modal-inner">, not the <drupal-dialog id="drupal-modal"> wrapper — two ids are required.

ARIA — role="alertdialog"

Wire log