Remote dialog
remote-dialog is the canonical pattern for server-owned modal
flows: the trigger button fetches a complete <dialog> fragment from
the server, the installRemoteDialog behavior opens it, and
installCloseDialog closes it again when the form inside submits
successfully.
Basic usage
Section titled “Basic usage”A trigger and a host element:
<button class="hc-button" data-hx-get="/items/123/edit" data-hx-target="#dialog-root" data-hx-swap="innerHTML"> Edit</button>
<div id="dialog-root" data-hc-remote-dialog-root></div>The host element has two markers:
id="dialog-root"— htmx swap target.data-hc-remote-dialog-root— opt-in for theinstallRemoteDialogbehavior.
The server returns a complete dialog:
<!-- GET /items/123/edit --><dialog class="hc-dialog"> <header class="hc-dialog__header"> <h2 class="hc-dialog__title">Edit item</h2> </header>
<form class="hc-form" data-hx-post="/items/123" data-hx-target="closest dialog" data-hx-swap="outerHTML" data-hc-close-dialog-on-success> <div class="hc-dialog__body"> <div class="hc-field"> <label class="hc-field__label" for="name">Name</label> <input id="name" class="hc-input" name="name" value="Acme"> </div> </div>
<footer class="hc-dialog__footer"> <button class="hc-button" type="button" onclick="this.closest('dialog').close()">Cancel</button> <button class="hc-button" data-variant="primary" type="submit">Save</button> </footer> </form></dialog>What happens
Section titled “What happens”- The user clicks Edit. htmx sends
GET /items/123/edit. - htmx swaps the response into
#dialog-root(innerHTML). htmx:afterSwapfires on#dialog-root. TheinstallRemoteDialogbehavior seesdata-hc-remote-dialog-rooton the target, finds the first<dialog>descendant, and callsshowModal().- The user edits and submits the form. The form has
data-hx-post="/items/123"plusdata-hc-close-dialog-on-success. - The server returns either the updated row HTML (swapped into
closest dialog— replacing the dialog) or a 4xx with the form re-rendered with validation errors (still inside the dialog). - On 2xx,
installCloseDialogsees the success and callsdialog.close(). The dialog disappears.
If the swap into the dialog already replaces the dialog element
itself (the closest dialog target with outerHTML does that),
installCloseDialog is a no-op for that request — the dialog is
already gone.
Server response contract
Section titled “Server response contract”| When | Return |
|---|---|
| Initial trigger (GET) | A full <dialog class="hc-dialog">…</dialog> fragment. |
| Successful submit (POST) | The updated content for the page target (e.g. the row), via the dialog’s data-hx-target / data-hx-swap. |
| Validation failure (4xx) | The form re-rendered with data-invalid="true" and aria-invalid="true" on the bad fields. The dialog stays open because installCloseDialog only closes on 2xx. |
For server-driven toast feedback include an HX-Trigger header on
the success response:
HX-Trigger: {"hc:toast":{"message":"Saved.","variant":"success"}}Accessibility
Section titled “Accessibility”- The dialog uses the native
<dialog>element withshowModal(). The browser handles focus trap, Escape-to-close, and inert background. - Declare a labeled title (
#dialog-title) and either:- Set
aria-labelledby="dialog-title"on the<dialog>, or - Make the title the first focusable element so AT picks it up.
- Set
- The Cancel button should also close the dialog. The recipe above
uses an inline
onclickfor brevity; in a real app prefer a small helper that listens forclickon[data-hc-dismiss-dialog].
Progressive enhancement
Section titled “Progressive enhancement”- Without htmx,
<button data-hx-get="…">does nothing. To keep the Edit action working without JavaScript, render the button inside an<a href="/items/123/edit">link that loads a full-page edit screen. The htmx attributes can still live on the link; htmx intercepts when present, and the anchor’shrefis the fallback. - Without
hc.behaviors.js, the dialog fragment lands in the page but does not auto-open. Add a<script>right after the<dialog>in the server response that opens it — e.g.<script>document.currentScript.previousElementSibling.showModal()</script>— or fall back to inline rendering.
Related
Section titled “Related”- Dialog component
- Confirm action recipe — pre-flight confirm without the round trip.
- Filter popover recipe — non-modal sibling.