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.
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 withhx-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
<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-confirmas protection; it's a convenience, not security.
Key points
- Empty 200 +
outerHTMLremoves the target;swap:gives CSS time to fade it. hx-includeon 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.