htmx: Build Interactive Web Apps with HTML · Build a Contacts App

Deleting and Bulk Actions

Delete a contact with confirmation and a fade-out, then select several rows with checkboxes and delete them in one request.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A Delete button on each row (with confirmation and a fade-out), plus checkboxes and a Delete selected button that removes several contacts at once.

The idea

  • Single delete: hx-delete="/contacts/1", hx-confirm="...", hx-target="closest tr", hx-swap="outerHTML swap:400ms". The server returns an empty 200 so the row is replaced by nothing. (A 204 isn't swapped, so the row would stay.)
  • Bulk delete: the checkboxes share name="ids". A button with hx-include="#rows" sends the inputs inside the table body and, exactly like a form, only checked checkboxes are sent. The server deletes those ids and returns the remaining rows for the <tbody>.

The code

Delete one or manyTry it yourself
<style>tr.htmx-swapping td { opacity: 0; transition: opacity 400ms; }</style>
<table>
  <tbody id="rows"></tbody>
</table>
<button hx-post="/contacts/bulk-delete" hx-include="#rows" hx-target="#rows"
        hx-confirm="Delete the selected contacts?">Delete selected</button>
<div hx-get="/contacts" hx-trigger="load" hx-target="#rows"></div>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  let contacts = { 1: "Ada Lovelace", 2: "Alan Turing", 3: "Grace Hopper", 4: "Linus Torvalds" };
  const rows = () => Object.entries(contacts).map(([id, name]) => `
    <tr><td><input type="checkbox" name="ids" value="${id}" aria-label="Select ${server.escape(name)}"></td>
        <td>${server.escape(name)}</td>
        <td><button hx-delete="/contacts/${id}" hx-confirm="Delete ${server.escape(name)}?"
                    hx-target="closest tr" hx-swap="outerHTML swap:400ms">Delete</button></td></tr>`).join("");
  server.get("/contacts", rows);
  server.delete("/contacts/:id", (req) => { delete contacts[req.params.id]; return ""; });
  server.post("/contacts/bulk-delete", (req) => {
    [].concat(req.body.ids || []).forEach((id) => delete contacts[id]);
    return rows();
  });
</script>

Bulk delete uses POST to an action URL rather than DELETE with a body, because many servers and proxies ignore bodies on DELETE requests, and htmx 2 sends DELETE values in the URL.

Common mistakes

  • Returning 204 from the delete endpoint: htmx doesn't swap 204, so the row stays.
  • Forgetting authorisation: the server must check that the user may delete each id it receives.
  • A bulk action with nothing selected: return the rows unchanged (or a message) instead of an error.
  • Relying on hx-confirm as protection; it's a convenience, not security.

Key points

  • Empty 200 + outerHTML removes the target; swap: gives CSS time to fade it.
  • hx-include on the container (or a selector like [name=ids]:checked) collects the selected rows.
  • Bulk actions are POSTs that return the updated list.

Further reading: htmx example: delete row · htmx example: bulk update · hx-confirm

Practice questions

Why should a delete endpoint used with hx-swap="outerHTML" return an empty 200 instead of 204?

Answer

htmx doesn't swap 204 responses, so the row would stay; an empty 200 replaces the row with nothing.

How do you send the ids of all checked rows with one button?

Answer

Give the checkboxes the same name and include their container (hx-include="#rows") or a selector such as [name=ids]:checked; only checked boxes are sent, as in a form.

Is hx-confirm enough to protect a destructive action?

Answer

No. It only asks the user; the server must still authenticate, authorise and validate every request.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?