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.
Basic usage
Section titled “Basic usage”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.)
<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>What happens:
- The user clicks the button.
- The behavior intercepts the click and shows the shared confirm dialog. The original click is suppressed; htmx does not fire yet.
- If the user confirms, the behavior dispatches a bubbling
hc:confirmedevent on the original button. - htmx, listening via
data-hx-trigger="hc:confirmed", sends the request. - The server returns HTML for the target area (and optionally
HX-Triggerevents).
If the user cancels, no event is dispatched and nothing happens.
Customizing the dialog
Section titled “Customizing the dialog”The message comes from data-hc-confirm. The remaining attributes are
optional:
| Attribute | Default |
|---|---|
data-hc-confirm | (required) dialog body text |
data-hc-confirm-title | Confirm |
data-hc-confirm-label | Confirm |
data-hc-cancel-label | Cancel |
data-hc-confirm-variant | source’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>Expanded HTML
Section titled “Expanded HTML”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>Macro form
Section titled “Macro form”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:
| Attribute | Default | Maps to |
|---|---|---|
action | (required) | URL for the htmx request |
method | post | data-hx-{method} |
target | (omitted) | data-hx-target |
swap | outerHTML | data-hx-swap |
variant | (omitted) | data-variant on the button |
message | Continue? | data-hc-confirm |
title | (omitted) | data-hc-confirm-title |
confirm-label | (omitted) | data-hc-confirm-label |
cancel-label | (omitted) | data-hc-cancel-label |
disabled-elt | this | data-hx-disabled-elt |
indicator | closest .hc-action | data-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 thehc:confirmedevent — dispatched on the trigger element when the user confirms — starts the request. An element withdata-hc-confirmbut 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, sohx-confirmnever 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.
Hyperscript alternative
Section titled “Hyperscript alternative”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
_hyperscriptfor 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.
Server response contract
Section titled “Server response contract”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
outerHTMLof 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-Retargetto steer the swap.
Accessibility notes
Section titled “Accessibility notes”- The shared dialog uses the standard
<dialog>element opened viashowModal(). The browser handles focus trapping, Escape-to-close, andinertfor content behind the dialog. - The dialog declares
aria-labelledby(title) andaria-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 putautofocus(ortabindex="-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.
Progressive enhancement
Section titled “Progressive enhancement”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 nohc:confirmedevent and does nothing. Adding a<noscript>notice or rendering a fully server-side confirmation page is the recommended graceful degradation.
Related
Section titled “Related”- Button — the visual surface for the action.
request-actionrecipe — the non-confirming version with spinner.live-searchrecipe — uses the same htmx trigger model.