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.

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

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

Load and post commentsTry it yourself
<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-target with an id that doesn't exist on the page: htmx fires htmx:targetError and nothing changes.
  • Forgetting that innerHTML is the default: hx-get on a button replaces the button's label unless you set a target.

Key points

  • Request attribute + hx-target + hx-swap describe 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.

Was this page helpful?