Guide

HTMX fundamentals explained

A warehouse manager clicks “Refresh status” and the order table updates without a full page reload — yet there is no React bundle, no client-side router, and no JSON serializer mapping DTOs to components. The server returns an HTML fragment; the browser swaps it in. That is the hypermedia-driven model behind HTMX: extend HTML with hx-* attributes so any element can trigger HTTP requests and patch the DOM from server-rendered responses. You keep business logic on the server (Django templates, Go handlers, Rails views) and ship a thin JavaScript layer (~14 KB gzipped) instead of a SPA toolchain. This guide covers core attributes, swap strategies, boosted navigation, out-of-band updates, extensions, a Harbor Supply order dashboard worked example, an HTMX vs SPA decision table, common pitfalls, and a practitioner checklist — alongside our rendering modes guide, Django guide, and REST API design guide.

What HTMX is and what problem it solves

Traditional multi-page apps (MPAs) reload the entire document on every action. Single-page apps (SPAs) avoid reloads but push state management, routing, and data fetching into JavaScript — often React or Vue with REST or GraphQL APIs returning JSON. HTMX occupies a third lane: partial page updates driven by HTML over the wire.

The browser already knows how to render HTML, follow links, and submit forms. HTMX adds:

  • AJAX from any element — not just <form> and <a> with custom JS.
  • Targeted DOM swaps — replace a table body, append a chat message, or remove a row.
  • Declarative triggers — click, submit, load, or custom events like revealed for infinite scroll.
  • Request metadata — HTMX sends headers (HX-Request, HX-Target) so servers can branch between full-page and partial responses.

The result feels interactive like a SPA but your canonical UI is still server templates. That cuts bundle size, simplifies SEO for content-heavy pages, and lets backend engineers own the full request-response cycle without maintaining parallel TypeScript types.

When HTMX is a strong fit

  • Internal tools and admin dashboards — CRUD tables, filters, inline edits.
  • Content sites with sprinkles of interactivity — comments, likes, live search.
  • Teams fluent in server frameworks — Django, Flask, Rails, Laravel, Go html/template.
  • Progressive enhancement — forms work without JS; HTMX layers speed on top.

Reach for a full SPA when you need offline-first PWA behavior, complex client-only state (canvas editors, real-time collaborative cursors), or a mobile app sharing logic via React Native.

Core attributes: requests, targets, and swaps

HTMX behavior is controlled by attributes on the element that initiates the request (or an ancestor via inheritance). The essential quartet:

  • hx-get / hx-post / hx-put / hx-delete — URL and HTTP method.
  • hx-target — CSS selector of the element to update (default: the element itself).
  • hx-swap — how to insert the response HTML (default: innerHTML).
  • hx-trigger — event that fires the request (default: click for buttons, submit for forms).
<button
  hx-get="/orders/42/status"
  hx-target="#order-status"
  hx-swap="innerHTML">
  Refresh status
</button>
<div id="order-status">Pending</div>

The server endpoint /orders/42/status returns a small HTML snippet — not JSON. HTMX replaces the inner HTML of #order-status with that snippet.

Swap modes you will use daily

hx-swapEffect
innerHTMLReplace children of target (most common).
outerHTMLReplace the target element itself.
beforeendAppend inside target (chat logs, infinite lists).
deleteRemove target on success (delete row).
noneFire request but do not swap (side-effect only).

Add hx-swap="innerHTML swap:300ms" for a CSS transition hint, or scroll:bottom to keep a chat pane pinned. Use hx-indicator to show a spinner element while the request is in flight.

Forms and CSRF

HTMX submits forms like a normal browser would, including file uploads with multipart/form-data. Include CSRF tokens in hidden fields — Django’s {% csrf_token %} works unchanged. For JSON APIs you would need custom headers; HTMX shines when the server speaks HTML.

Triggers, boosting, and out-of-band swaps

hx-trigger accepts modifiers for debouncing and polling:

<input
  name="q"
  hx-get="/search"
  hx-trigger="keyup changed delay:300ms"
  hx-target="#results">

changed skips duplicate values; delay:300ms debounces keystrokes. Use every 5s for lightweight polling dashboards, or pair with the SSE extension for server push.

hx-boost: SPA-like navigation without a router

Add hx-boost="true" on <body> or a nav container. HTMX intercepts link clicks and form submissions, fetches pages via AJAX, and swaps the <body> content — updating title and history via the History API. You keep traditional server routing; the user perceives faster transitions. Ensure each page returns a full body fragment when HX-Request: true is present, or use hx-select to extract a subtree from a full HTML document.

Out-of-band (OOB) swaps

One response can update multiple DOM regions. Elements in the response with hx-swap-oob="true" and a matching id swap into place outside the primary target:

<!-- Primary swap into #cart-items -->
<tr id="line-7">...</tr>
<!-- OOB update for cart badge -->
<span id="cart-count" hx-swap-oob="true">3</span>

OOB is how you refresh a header notification count while appending a table row — without client-side state synchronization.

Server-side patterns and the HX-Request header

Production HTMX backends follow a simple contract:

  1. Detect partial requests via the HX-Request header (value true).
  2. Return an HTML fragment for HTMX; return a full page (or redirect) for normal navigation.
  3. Use HX-Redirect response header to force a full navigation after login or checkout.
  4. Return 422 or 400 with an error fragment to display inline validation.

In Django, a view might render orders/_row.html for HTMX and orders/detail.html otherwise. In Go, check r.Header.Get("HX-Request") before choosing a template. This mirrors patterns in our Django guide but trades JSON serializers for partial templates.

You do not need a separate REST API unless mobile clients or third parties consume JSON. Many teams run HTMX for the web UI and a thin JSON API only where required — avoiding the “two apps in one repo” drift common in SPA + API splits.

Worked example: Harbor Supply order dashboard

Harbor Supply operators track wholesale orders from a single server-rendered page. Requirements: filter by status without reload, refresh one row’s fulfillment state, append audit-log entries, and show a loading spinner. No client build step.

Page skeleton

<form hx-get="/orders" hx-target="#order-table" hx-trigger="change">
  <select name="status">
    <option value="">All</option>
    <option value="pending">Pending</option>
    <option value="shipped">Shipped</option>
  </select>
</form>

<table>
  <tbody id="order-table">
    {% include "orders/_rows.html" %}
  </tbody>
</table>

Per-row refresh button

<tr id="order-{{ order.id }}">
  <td>{{ order.sku }}</td>
  <td>{{ order.status }}</td>
  <td>
    <button
      hx-get="/orders/{{ order.id }}/status"
      hx-target="closest tr"
      hx-swap="outerHTML"
      hx-indicator="#spin-{{ order.id }}">
      Refresh
    </button>
    <span id="spin-{{ order.id }}" class="htmx-indicator">...</span>
  </td>
</tr>

The status endpoint returns a full <tr> with updated cells. closest tr targets the row from inside the button. outerHTML replaces the entire row so status badges and action buttons stay consistent.

Audit log append

<div id="audit-log" hx-get="/orders/{{ id }}/audit"
     hx-trigger="every 30s" hx-swap="beforeend">
  ...existing entries...
</div>

Polling every 30 seconds appends new log lines. For near-real-time, the SSE extension (hx-ext="sse") streams events from /orders/{{ id }}/stream without inventing a WebSocket client in application code.

Total custom JavaScript: one <script src="htmx.min.js"> tag. Styling uses existing CSS; no Vite, no hydration mismatch, no API versioning for the admin UI alone.

Approach decision table

ScenarioPrefer HTMXPrefer SPA (React/Vue)
Admin CRUD dashboardYesRarely
Marketing blog with live searchYesOptional
Offline mobile PWANoYes
Collaborative whiteboardNoYes
Team knows Django/Rails, not TypeScriptYesCostly ramp
Public JSON API + web + mobileWeb onlyShared API layer
Sub-100ms filter on 10k-row tableServer paginationClient virtual list
Strictest SEO for dynamic contentServer HTML winsSSR framework needed

HTMX pairs well with Astro (static shell + HTMX islands) and with Tailwind for styling fragments. It does not replace WebSockets for bidirectional games — use extensions or a thin JS module where needed.

Common pitfalls

  • Returning full HTML pages to partial requests — duplicates layout chrome inside a table cell. Branch on HX-Request.
  • Missing CSRF on POST — HTMX does not exempt you from framework security tokens.
  • Breaking the back button with boost — test history navigation; use hx-push-url deliberately.
  • ID collisions after OOB swaps — duplicate id attributes break targeting. Keep IDs unique per fragment.
  • Giant fragments — swapping 500 KB of HTML is slower than JSON + client render. Paginate server-side.
  • No loading or error UX — use hx-indicator and handle 4xx/5xx with htmx:responseError listeners.
  • Assuming HTMX replaces validation — server-side validation remains mandatory; fragments should echo field errors.
  • Inlining business logic only in templates — keep domain logic in services; templates stay dumb.

Practitioner checklist

  • Include htmx.min.js via CDN or self-host with SRI hash; pin a version in production.
  • Define partial templates (_row.html, _results.html) separate from full layouts.
  • Branch views on HX-Request; integration-test both full page and fragment paths.
  • Set meaningful hx-target and hx-swap per interaction; document non-obvious triggers.
  • Add indicators for requests expected to exceed 200 ms.
  • Use hx-confirm for destructive actions (delete row, cancel order).
  • Configure Content-Security-Policy to allow inline styles HTMX may toggle; see our CSP guide.
  • Measure Time to Interactive — HTMX should improve it vs a heavy SPA baseline.
  • Plan escape hatches: a complex widget can be a small Alpine or React island without rewriting the app.
  • Log fragment render time server-side; slow partials hurt perceived interactivity.

Key takeaways

  • HTMX turns HTML into a hypermedia client — server-rendered fragments replace most AJAX + JSON + client templating.
  • hx-get, hx-target, hx-swap, and hx-trigger cover the majority of dashboard and form interactions.
  • hx-boost and OOB swaps deliver SPA-like UX while keeping routing on the server.
  • The HX-Request header is the contract between browser and backend for partial vs full responses.
  • Choose HTMX for server-strong teams and content-heavy interactivity; choose SPAs when client-only complexity dominates.

Related reading