htmx: Build Interactive Web Apps with HTML · Beyond the Basics

Choosing htmx: Architecture Decisions

Decide when htmx is the right architecture, when a JavaScript framework is, and how to combine them in one product.

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

What you'll build

A decision record for a new product with three parts, an admin area, a public catalogue and a drag-and-drop schedule planner, choosing the right approach for each.

The idea

htmx moves state and rendering to the server. That's a strength when the server is the source of truth and the UI mostly shows and edits that data. It's a weakness when the UI must react instantly to every mouse move, work offline, or share a JSON API with mobile apps.

Question Points to htmx Points to a JS framework
Where does the truth live? the server database the browser (offline, local edits)
How often does the UI change without the server? rarely constantly (drag, draw, edit)
Team skills strong backend strong frontend
Other clients (mobile)? none, or separate API shared JSON API already needed
Long-term maintenance fewer dependencies, no build larger ecosystem, more tooling

Mixing is normal: htmx for most pages, and a React, Vue or Svelte island for the one complex widget, mounted on a <div> in a server-rendered page.

The code

A hybrid page: htmx handles the server-driven list; a tiny client-side widget (standing in for a framework island) handles instant interaction and saves through a normal htmx request:

htmx page with a client-side islandTry it yourself
<h4>Today's schedule</h4>
<ul id="slots" hx-get="/slots" hx-trigger="load, slotsChanged from:body"></ul>

<div id="planner">
  <input id="time" type="range" min="8" max="18" value="9">
  <output id="shown">09:00</output>
  <button id="book">Book this slot</button>
</div>
<script>
  // The "island": instant feedback in the browser, no request per movement.
  const time = document.getElementById("time"), shown = document.getElementById("shown");
  time.addEventListener("input", () => { shown.textContent = String(time.value).padStart(2, "0") + ":00"; });
  document.getElementById("book").addEventListener("click", () =>
    htmx.ajax("POST", "/slots", { values: { hour: time.value }, swap: "none", source: "#book" }));
</script>

<!-- The pretend server: on a real site this part is your backend (Python, PHP, Node, Go...) -->
<script>
  const booked = [10];
  server.get("/slots", () => booked.sort((a, b) => a - b).map((h) => `<li>${String(h).padStart(2, "0")}:00 booked</li>`).join(""));
  server.post("/slots", (req) => { booked.push(Number(req.body.hour)); return { body: "", headers: { "HX-Trigger": "slotsChanged" } }; });
</script>

For the example product: the admin area and catalogue are classic htmx territory (forms, tables, search, SEO-friendly pages); the schedule planner is an island with a client-side library that saves through the same server endpoints.

Common mistakes

  • Choosing by hype in either direction. Decide per feature, using the questions above.
  • Rebuilding complex client-side widgets with dozens of htmx requests per second.
  • Building a JSON API and server templates for the same screens "just in case": two sources of truth.
  • Forgetting that htmx pages are server-rendered by default, which helps SEO and first load.

Key points

  • htmx fits server-owned data and form/table-heavy UIs.
  • Frameworks fit highly interactive, offline or multi-client UIs.
  • Islands let both live in one product.

Further reading: htmx essay: When should you use hypermedia? · htmx JavaScript API: htmx.ajax

Practice questions

Name two signs that a feature is a poor fit for htmx alone.

Answer

The UI changes continuously without the server (drag-and-drop, drawing), or it must work offline / share a JSON API with other clients.

What is an 'island' in a mostly htmx app?

Answer

A self-contained client-side widget (React, Vue, Svelte or plain JS) mounted inside a server-rendered page, talking to the same server.

How did the island in the example tell the htmx list to refresh?

Answer

Its POST response carried HX-Trigger: slotsChanged, and the list listens with hx-trigger="slotsChanged from:body".

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

Was this page helpful?