Skip to content

Confirm action

confirm-action adds a modal confirmation step in front of a destructive htmx request. The button stays a normal <button>; htmx remains the sole owner of the network request; a small behavior shim opens the dialog and re-emits a hc:confirmed event when the user accepts.

Add data-hc-confirm to any element that issues an htmx request, and gate the htmx trigger on hc:confirmed:

<button
class="hc-button"
data-variant="error"
data-hc-confirm="Delete this item?"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML">
Delete
</button>

Try it — clicking Delete opens the shared confirm dialog. (This demo has no server, so confirming just reports the outcome instead of firing an htmx request.)

What happens:

  1. The user clicks the button.
  2. The behavior intercepts the click and shows the shared confirm dialog. The original click is suppressed; htmx does not fire yet.
  3. If the user confirms, the behavior dispatches a bubbling hc:confirmed event on the original button.
  4. htmx, listening via data-hx-trigger="hc:confirmed", sends the request.
  5. The server returns HTML for the target area (and optionally HX-Trigger events).

If the user cancels, no event is dispatched and nothing happens.

The message comes from data-hc-confirm. The remaining attributes are optional:

AttributeDefault
data-hc-confirm(required) dialog body text
data-hc-confirm-titleConfirm
data-hc-confirm-labelConfirm
data-hc-cancel-labelCancel
data-hc-confirm-variantsource’s data-variant, else primary
<button
class="hc-button"
data-variant="error"
data-hc-confirm="Permanently delete invoice #123? This cannot be undone."
data-hc-confirm-title="Delete invoice"
data-hc-confirm-label="Delete"
data-hc-cancel-label="Keep"
data-hx-delete="/invoices/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr">
Delete
</button>

Wrapping the button in .hc-action with an htmx indicator gives the full destructive-action experience from plan §24:

<span class="hc-action">
<button
class="hc-button"
data-variant="error"
type="button"
data-hc-confirm="Delete this item?"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
data-hx-disabled-elt="this"
data-hx-indicator="closest .hc-action">
Delete
</button>
<span class="hc-spinner htmx-indicator" aria-hidden="true"></span>
</span>

If you import @hypermedia-components/core/macros, you can write the same pattern as a single custom element:

<hc-confirm-action
method="delete"
action="/items/123"
target="closest tr"
swap="outerHTML"
variant="error"
message="Delete this item?"
confirm-label="Delete"
cancel-label="Keep">
Delete
</hc-confirm-action>

The element runs once on connectedCallback and replaces its children with the expanded HTML shown above, then calls htmx.process(this) so htmx picks up the new attributes. The upgrade is idempotent — re-attaching the element does not re-expand it.

Attributes:

AttributeDefaultMaps to
action(required)URL for the htmx request
methodpostdata-hx-{method}
target(omitted)data-hx-target
swapouterHTMLdata-hx-swap
variant(omitted)data-variant on the button
messageContinue?data-hc-confirm
title(omitted)data-hc-confirm-title
confirm-label(omitted)data-hc-confirm-label
cancel-label(omitted)data-hc-cancel-label
disabled-eltthisdata-hx-disabled-elt
indicatorclosest .hc-actiondata-hx-indicator
no-spinner(boolean)Omit .hc-spinner if set

The macro is optional — the documented contract is the expanded HTML. If you need to step outside the macro’s attribute surface (custom button content, an extra wrapper class, …), copy the expanded HTML and edit it directly.

Interaction with htmx — the specification

Section titled “Interaction with htmx — the specification”

The contract between the confirm gate and htmx, spelled out (these are browser-tested guarantees, not implementation details):

  • The click is intercepted in the capture phase with preventDefault + stopPropagation, so htmx (and any other click handler on the element) never observes the original activation.
  • data-hx-trigger="hc:confirmed" is therefore required: only the hc:confirmed event — dispatched on the trigger element when the user confirms — starts the request. An element with data-hc-confirm but a default click trigger never fires its htmx request.
  • Cancel dispatches nothing. No request, no event.
  • Do not combine with htmx’s own hx-confirm — htmx never sees the activation, so hx-confirm never runs; you’d be writing dead attributes. Use one mechanism per element.
  • The interception is delegated at the document level, so triggers swapped in by htmx are gated without re-initialization.

See also htmx integration → Confirm gating.

If you prefer to keep the orchestration next to the markup, the same flow can be expressed in _hyperscript instead of relying on installConfirm(). Drop the data-hc-confirm attribute, write the confirm step inline, and dispatch the same hc:confirmed event that htmx already listens for:

<button
class="hc-button"
data-variant="error"
data-hx-delete="/items/123"
data-hx-trigger="hc:confirmed"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
_="on click
if confirm('Delete this item?')
send hc:confirmed to me
end">
Delete
</button>

confirm(...) here is the browser’s blocking primitive, which is unstyled and not always desirable for destructive actions. To reuse the shared <dialog class="hc-confirm-dialog"> markup from a single page-level element, see the hyperscript integration page.

When to prefer this form:

  • The confirm step is genuinely one-off (a single delete button in the page chrome) and you don’t want to import installConfirm.
  • You already include _hyperscript for other reasons.

When to stick with vanilla:

  • Several elements need the same confirm flow — one installConfirm() call covers all of them.
  • You want the shared styled dialog without rendering the markup yourself.

The server’s response is whatever the htmx attributes specify — the confirm behavior does not change the request shape.

For a row-level delete:

  • On success: return HTML for the target area, or an empty body with a status that lets htmx swap nothing. The recipe above swaps outerHTML of the closest <tr>, so the server returns the new row HTML (or nothing if the row is gone).
  • For a toast notification: include HX-Trigger: {"hc:toast":{"message":"Deleted","variant":"success"}} in the response headers.
  • On validation failure: return a 4xx with HTML describing the failure, or use HX-Reswap / HX-Retarget to steer the swap.
  • The shared dialog uses the standard <dialog> element opened via showModal(). The browser handles focus trapping, Escape-to-close, and inert for content behind the dialog.
  • The dialog declares aria-labelledby (title) and aria-describedby (message), so screen readers announce both on open.
  • Focus moves to the Cancel button by default — destructive actions must require explicit confirmation, not a stray Enter keypress.
  • On close — confirm, cancel, or Escape — the native dialog.close() returns focus to the trigger that opened it; the behavior adds no focus code of its own. If the confirmed request then removes that trigger from the DOM (deleting a table row is the canonical case), place the next focus target deliberately in the swap: target the surrounding region and put autofocus (or tabindex="-1" plus a server-driven focus hint) on the element keyboard users should land on, so they are not silently dropped at <body>.
  • The original button keeps its native <button> semantics. Use a visible, descriptive label (Delete, Discard changes) rather than an icon alone.

Without the behavior loaded, the button still works:

  • Without htmx, the button performs no action — exactly as the markup describes. A safer non-JS fallback is to put the button inside a <form method="post" action="/items/123/delete">; submission will still go through, just without the confirmation step.
  • Without hc.behaviors.js, the click bubbles normally. htmx sees no hc:confirmed event and does nothing. Adding a <noscript> notice or rendering a fully server-side confirmation page is the recommended graceful degradation.
  • Button — the visual surface for the action.
  • request-action recipe — the non-confirming version with spinner.
  • live-search recipe — uses the same htmx trigger model.