htmx: Build Interactive Web Apps with HTML · Build a Contacts App
Adding Contacts and Updating Several Places
Add a contact from a form, append the new row, update a counter in the header out of band, and notify other parts of the page with HX-Trigger.
What you'll build
An Add contact form under the table. Saving appends the new row, updates the "N contacts" counter in the page header, clears the form, and shows a short confirmation, all from one request.
The idea
Two techniques update more than the target:
- Out-of-band swaps: an element in the response with
hx-swap-oob="true"and anidreplaces the element with thatidanywhere on the page. Good when the server knows exactly which element changes. HX-Triggerresponse header: the server fires an event (contactAdded); any element can listen withhx-trigger="contactAdded from:body"and refresh itself. Good when several independent widgets care.
The code
<header>Contacts (<span id="contact-count">2</span>) <small id="toast"></small></header>
<table><tbody id="rows">
<tr><td>Ada Lovelace</td><td>[email protected]</td></tr>
<tr><td>Alan Turing</td><td>[email protected]</td></tr>
</tbody></table>
<form hx-post="/contacts" hx-target="#rows" hx-swap="beforeend"
hx-on::after-request="if (event.detail.successful) this.reset()">
<input name="name" placeholder="Name" required>
<input name="email" type="email" placeholder="Email" required>
<button>Add contact</button>
</form>
<p hx-get="/contacts/latest" hx-trigger="contactAdded from:body">Latest: Alan Turing</p>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
const contacts = [["Ada Lovelace", "[email protected]"], ["Alan Turing", "[email protected]"]];
server.post("/contacts", (req) => {
contacts.push([req.body.name, req.body.email]);
return {
status: 201,
headers: { "HX-Trigger": "contactAdded" },
body: `<tr><td>${server.escape(req.body.name)}</td><td>${server.escape(req.body.email)}</td></tr>
<span id="contact-count" hx-swap-oob="true">${contacts.length}</span>`,
};
});
server.get("/contacts/latest", () => "Latest: " + server.escape(contacts[contacts.length - 1][0]));
</script>
Common mistakes
- Returning a
<tr>as an out-of-band element: browsers drop table rows outside a table while parsing. Wrap OOB rows in<template>. - An OOB element without an
id(or with an id that isn't on the page): nothing happens. - Coupling everything to the form's response: when a fourth widget needs updating, add an
HX-Triggerlistener instead of growing the response.
Key points
- One request can update many places: OOB swaps for known elements,
HX-Triggerfor independent listeners. - Clear the form only after a successful request.
- Answer creates with 201; htmx swaps any 2xx.
Further reading: hx-swap-oob · HX-Trigger response header
Practice questions
What does hx-swap-oob="true" on an element in a response do?
Answer
htmx removes it from the main response and swaps it into the page element with the same id, in addition to the normal swap.
When is HX-Trigger a better choice than an out-of-band swap?
Answer
When several independent parts of the page should react; each listens for the event and refreshes itself, so the endpoint doesn't need to know about them.
Why might an out-of-band <tr> silently fail?
Answer
The HTML parser drops <tr> elements outside a table; wrap them in a <template> tag in the response.
Open in the app to mark lessons complete, take notes and track your progress.