Skip to content

Undo delete

undo-delete is the blessed undo instead of confirm pattern for frequent destructive actions: no dialog — the delete executes immediately, the server keeps the item recoverable for a grace period, and the result toast carries an Undo button that puts the row back exactly where it was. It is the counterpart to confirm-action, built from already-shipped pieces with zero new JavaScript, and stable under the markup versioning policy.

It needs installToast() (in the auto-init ./behaviors bundle) — nothing else.

Also known as: soft delete, undo toast.

Click Delete — no confirmation — then Undo in the toast within 10 seconds to put the row back. The server keeps a 30-second grace window (threaded statelessly through the tombstone’s restore URL); after it expires the restore comes back as a “Too late” error toast. The endpoints live under api/recipes/undo-delete/; Reset rows refetches the canned rows.

ItemActions

One button per item, in the request-action shape — deliberately without data-hc-confirm:

<tr id="item-42">
<td>Anvil</td>
<td>
<button class="hc-button" data-size="sm" type="button"
data-hx-delete="/items/42"
data-hx-target="closest tr"
data-hx-swap="outerHTML"
data-hx-disabled-elt="this">Delete</button>
</td>
</tr>

The DELETE response replaces the row with a hidden tombstone that preserves the DOM slot and carries the restore wiring, and triggers the undo toast:

<tr id="item-42" hidden
data-hx-post="/items/42/restore"
data-hx-trigger="item-42:restore from:body"
data-hx-swap="outerHTML"></tr>
HX-Trigger: {"hc:toast":{"id":"undo-item-42","message":"\"Anvil\" deleted",
"variant":"info","duration":10000,
"action":{"label":"Undo","event":"item-42:restore"}}}

The pairing key (item-42:restore) is one server-generated string in exactly two places: the toast’s action.event and the tombstone’s trigger. Clicking Undo makes the toast dispatch that bubbling event (the shipped toast action button); the tombstone hears it via from:body and POSTs the restore; the server returns the original row, which swaps back into the same slot — position preserved, several pending undos never cross.

The restore response reuses the toast id, so the undo toast updates in place to “restored”. Use <tr hidden> in tables, <li hidden> in lists, <div hidden> in card grids.

The server hard-deletes on its own clock (recommended: grace ≥ toast duration, e.g. 10 s toast / 30–60 s grace). A dismissed or expired toast finalizes nothing. Restore after expiry follows the 200-with-truth doctrine: body = the tombstone again, plus an error toast (“permanently deleted”) — no status-code choreography, same as datagrid-bulk-actions.

RequestResponse
DELETE /items/42200 + the hidden tombstone (replaces the row via the button’s outerHTML swap) + the HX-Trigger undo toast — action.event carries the pairing key
POST /items/42/restore (within grace)200 + the original row — it swaps back into the same slot; reusing the toast id updates the undo toast in place. Restore is idempotent
POST /items/42/restore (grace expired)200 + the tombstone again + an error toast (“permanently deleted”) — 200-with-truth, no status-code choreography
No-JS deleteKeep a form fallback: method="post" + 303 back (the mutating-form branching); undo itself is toast-borne and honestly unavailable
confirm-actionundo-delete
Fitsrare, catastrophic, hard-to-reversefrequent, recoverable
Costa dialog on every actiona grace window on the server
Failure modeconfirm fatigueexpired grace (error toast)

Pick one per action — stacking both pays both costs and buys nothing.

Inside a datagrid the tombstone swaps in as a row and the tbody observer re-derives roles, offsets and selection on both delete and restore. Tombstones are inert leftovers pruned by any full re-render (a data-region refresh, pagination). Without JavaScript, keep a form-fallback delete (method="post" + 303, the mutating-form branching) — undo itself is toast-borne and honestly unavailable; give no-JS deletes a server-side confirm page if they need protection.

The claims here — position-preserving restore, pairing-key isolation across two pending undos, the expiry path — are pinned by a real-htmx browser test (test-browser/undo-delete.spec.mjs).

  • The undo toast is variant: "info"role="status" (polite); the expiry failure is variant: "error"role="alert". Standard toast semantics, unchanged.
  • The Undo button is a real <button> inside the toast — keyboard reachable while the toast is visible. Size the grace window to be comfortably longer than the toast, so a keyboard user who misses the button loses nothing they could not also lose by being slow.
  • The tombstone is hidden and never focusable; restoring swaps the row back without stealing focus.
  • The Delete button’s in-flight disabled (data-hx-disabled-elt="this") is the native attribute, reported by assistive tech.