Confirm dialog

Confirm dialog interactive ControllerEndpoint

Designed replacement for window.confirm — every hx-confirm upgrades automatically.

Layer: L1 surface · Recipe: confirm-affordance — confirm irreversible action. Curriculum: AGENTS.md; pick matrix: docs/agent/pick-a-surface.md; blast radius: CONSUMER_MAP.md.

Copy this

html
<button class="button" data-variant="destructive" hx-delete="/mock/noop" hx-confirm="Delete this invoice? This cannot be undone.">Delete invoice</button>

Server exchange

When the client affordance finishes (click, confirm, keystroke…), htmx issues this request. Your API must return the response fragment in the table — usually HTML, not JSON (unless the partial says otherwise). Dazzle often renders these routes from the app model; a standalone HTMX4 app implements them explicitly. The Envelope column is the exchange envelope (part of the Swap contract) — what the response may re-emit relative to the persistent slot.

Do not reimplement the gallery. Flash toasts (e.g. “Deleted (demo).”), /mock/* paths, and other static-site scaffolding are demo-only (MOCK_HTMX in site/build_site.py). They are not Hyperpart surface and not a product API. If an agent is stuck “making the toast work,” stop — implement the exchange row below instead.

RequestTriggerResponse fragmentSwapEnvelopeStates
DELETE /app/invoices/{id}the button, after the user approves the designed confirm dialogthe server deletes the resource and returns the replacement markup for the affected region (e.g. the row's removal, or an empty-state). Not a toast — the gallery's 'Deleted (demo).' toast is MOCK_HTMX onlyper the button's `hx-target`/`hx-swap` (row removal by default)host_owned—

DELETE /app/invoices/{id} — example handler

HTMX4 / standalone HM: return HTML fragments. Dazzle often emits this for you from the model; agents building a plain FastAPI app should match this shape. Not a dual-lock module — application code. Not a gallery mock. Respect exchange envelope host_owned on the response.

python
# This is the DELETE after confirm — not a “confirm API”.
# The dialog is client-only (hx-confirm + confirm.js).
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()


@app.delete("/app/invoices/{invoice_id}", response_class=HTMLResponse)
def delete_invoice(invoice_id: str) -> str:
    # delete_invoice_from_db(invoice_id)
    # Return whatever hx-target/hx-swap expect, e.g.:
    #   - empty string with hx-swap='delete' on the trigger
    #   - an empty-state partial for the list region
    #   - OOB markup to refresh a sibling region
    return ""

Swap contract

Agent-visible HTMX topology (ADR-0054 / decision 0012). exchange envelope = what the response may re-emit relative to the persistent slot (body_only | outer | none | host_owned | document). dual-lock validates part markup only — not this envelope. Stem: stems/morph-safe-hypermedia.md; decision: docs/decisions/0012-swap-identity-contract.md.

Gallery mocks may approximate morph with innerHTML — production follows the Swap + Envelope columns in Server exchange.

Exchanges (swap · envelope)

  • DELETE /app/invoices/{id} → per the button's `hx-target`/`hx-swap` (row removal by default) · envelope=host_owned

Envelope rules

  • body_only — innerHTML / innerMorph into a slot; response is interior only (no re-wrap of slot id / nested data-dz-region).
  • outer — outerHTML / outerMorph; response may carry identity.
  • none — no HTML swap (JSON/204/bytes; client or OOB companion).
  • host_owned — swap target/mode chosen by the host button’s hx-target / hx-swap.
  • document — full navigation / document load (not a fragment).
  • Slot owns stable id / domain keys; state in DOM, not Alpine.

Envelope response examples

What the server returns for each exchange on Confirm dialog. Match the exchange envelope; dual-lock still applies to interior markup.

DELETE /app/invoices/{id} · envelope=host_owned

Correct responses for host_owned — follow the initiating control’s hx-target / hx-swap.

Do — correct response body

html
<!-- envelope=host_owned → match the *button's* hx-target / hx-swap -->
<!-- Example A: button hx-swap=delete → empty body (row removed) -->

<!-- Example B: button hx-target=#region-body hx-swap=innerHTML → body_only fragment -->
<div class="dz-list-row">remaining rows…</div>

<!-- Example C: button hx-swap=outerHTML on a card → full card root -->
<div class="dz-card" id="invoice-42">…updated card…</div>

Don’t — violates host_owned

html
<!-- WRONG: assuming a fixed envelope without reading the host affordance -->
<!-- e.g. always returning outer chrome when the button asked for delete/none -->
<div data-dz-region id="region-x">…</div>

How to use it

Seams

  • protocol: any hx-confirm → htmx:confirm → designed singleton dialog
  • message text IS the hx-confirm attribute value (author the string, not a dialog partial)
  • on accept: issueRequest on the underlying action; cancel: dropRequest
  • pick-a-surface: request gating vs dialog addressing (chrome-vs-protocol)

Do / Don't

DoDon't
put hx-confirm on the destructive action element; implement the DELETE (or other) endpoint as the Server exchangewire a bespoke dialog open/close for every delete button or invent a POST /confirm endpoint for the dialog itself
fleet upgrade: every hx-confirm gets the same chrome (gating)author a full <dialog> per delete when only yes/no is required

Pitfalls

  • hx-confirm is a client affordance — it needs no Exchange of its own (and no FastAPI route for “confirm”)
  • do not re-implement confirm with window.confirm (loses the designed dialog)
  • do not use confirm when you need a custom modal body — that is dialog (addressing)
  • gallery toast 'Deleted (demo).' is MOCK_HTMX scaffolding in site/build_site.py — not Hyperpart surface; production returns the DELETE fragment from the Exchange

Keyboard / AT

  • dialog traps focus; Esc / cancel dismisses without issuing the request
  • confirm control is keyboard-activatable (Enter/Space)

Related parts

button dialog

DOM contract

What the emitted HTML must satisfy — the table is the required surface; Python under contracts/ is the package-internal dual-lock CI runs (tests/test_contracts.py), not an app route. Standalone HTMX4: implement the API so responses match this markup. Dazzle: the agent emits SSR that already satisfies it. Do not invent attrs outside these tables. For request/response wiring see Server exchange.

contracts/confirm.py

Required in the DOM: root [hx-confirm] (part confirm). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[hx-confirm]hx-confirmpresent (any value)

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

python
"""HYPERPART: confirm — hx-confirm interceptor (client affordance).

Package-internal dual-lock for CI / validate_dom — not application business
code. To use confirm in an HTMX app: put ``hx-confirm="…"`` on the action
element and load the controller (``controllers/dz-confirm.js``). The dialog
itself is not server-rendered; after the user approves, htmx issues the
element's existing ``hx-*`` request (see the part page Server exchange).

In-contract: any element with ``hx-confirm``. Opt-out: ``data-dz-native-confirm``
(source token; gallery demos may strip the ``dz-`` prefix).
"""

from contracts._kit import DomContract, Node, Present

DOM_CONTRACT = DomContract(
    part="confirm",
    root="[hx-confirm]",
    nodes=(Node("[hx-confirm]", attrs={"hx-confirm": Present()}),),
)

__all__ = ["DOM_CONTRACT"]

Notes

**Pick:** request gating — yes/no before an existing hx-* runs (stem chrome-vs-protocol). Not a custom modal body (dialog = addressing). Protocol: confirm.js intercepts htmx:confirm; message IS the hx-confirm string; accept issues the underlying request. No confirm Exchange / no POST /confirm. Gallery-only toast: MOCK_HTMX may flash Deleted (demo). — not Hyperpart surface; production returns the action Exchange fragment.

Source files

One logical Hyperpart, 4 code items (CSS layered, JS bundled). Bound by HYPERPART: confirm — python tools/hyperpart.py confirm lists them.

site/registry.py · contracts/confirm.py · components/alert.css:239 · controllers/dz-confirm.js