htmx: Build Interactive Web Apps with HTML · Hypermedia Foundations
Requests, Targets and Swaps
Build a comments widget: load comments on demand, post a new one, and choose exactly where and how each response lands.
What you'll build
A comments section under an article: a Show comments button loads them, and a form posts a new comment that appears at the end of the list without reloading.
The idea
Three attributes do most of the work in htmx:
| Attribute | Question it answers | Default |
|---|---|---|
hx-get / hx-post / hx-put / hx-patch / hx-delete |
which request? | none |
hx-target |
where does the response go? | the element itself |
hx-swap |
how is it inserted? | innerHTML (replace the content) |
Useful hx-swap values: innerHTML, outerHTML (replace the element), beforeend (append inside),
afterbegin (prepend inside), delete, none.
Relative targets such as closest li, next .panel and find .count let the same markup work in a list of many
items. And htmx attributes in a response work immediately, so the server can return new buttons and forms.
The code
<article>
<h3>Why hypermedia?</h3>
<section id="comments">
<button hx-get="/articles/7/comments" hx-target="#comments">Show comments (2)</button>
</section>
</article>
<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
const comments = ["Great article!", "I'm trying htmx this weekend."];
const item = (text) => `<li>${server.escape(text)}</li>`;
server.get("/articles/7/comments", () => `
<ul id="comment-list">${comments.map(item).join("")}</ul>
<form hx-post="/articles/7/comments" hx-target="#comment-list" hx-swap="beforeend"
hx-on::after-request="if (event.detail.successful) this.reset()">
<input name="text" placeholder="Add a comment" required> <button>Post</button>
</form>`);
server.post("/articles/7/comments", (req) => {
comments.push(req.body.text);
return item(req.body.text); // just the new <li>: beforeend appends it
});
</script>
Notice the division of work: the GET returns the list and the form (with its own htmx attributes); the POST
returns only the new item, because hx-swap="beforeend" appends it.
Common mistakes
- Returning the whole list from the POST while using
beforeend: the comments appear twice. Match the response to the swap. - Using
hx-targetwith an id that doesn't exist on the page: htmx fireshtmx:targetErrorand nothing changes. - Forgetting that
innerHTMLis the default:hx-geton a button replaces the button's label unless you set a target.
Key points
- Request attribute +
hx-target+hx-swapdescribe every interaction. - Responses can contain more htmx, so the UI is driven by the server.
- Return the smallest HTML the swap needs.
Further reading: hx-target · hx-swap · Tutorial: Targets
Practice questions
Which hx-swap appends a new comment to the end of a list?
Answer
beforeend: it inserts the response inside the target, after its last child.
What is the default target and swap if you only write hx-get on a button?
Answer
The button itself, with innerHTML: the response replaces the button's content.
How can one row template's button update only its own row?
Answer
Use a relative target such as hx-target="closest tr" (with hx-swap="outerHTML"), so no unique ids are needed.
Open in the app to mark lessons complete, take notes and track your progress.