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
revealedfor 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:clickfor buttons,submitfor 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-swap | Effect |
|---|---|
innerHTML | Replace children of target (most common). |
outerHTML | Replace the target element itself. |
beforeend | Append inside target (chat logs, infinite lists). |
delete | Remove target on success (delete row). |
none | Fire 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:
- Detect partial requests via the
HX-Requestheader (valuetrue). - Return an HTML fragment for HTMX; return a full page (or redirect) for normal navigation.
- Use
HX-Redirectresponse header to force a full navigation after login or checkout. - Return
422or400with 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
| Scenario | Prefer HTMX | Prefer SPA (React/Vue) |
|---|---|---|
| Admin CRUD dashboard | Yes | Rarely |
| Marketing blog with live search | Yes | Optional |
| Offline mobile PWA | No | Yes |
| Collaborative whiteboard | No | Yes |
| Team knows Django/Rails, not TypeScript | Yes | Costly ramp |
| Public JSON API + web + mobile | Web only | Shared API layer |
| Sub-100ms filter on 10k-row table | Server pagination | Client virtual list |
| Strictest SEO for dynamic content | Server HTML wins | SSR 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-urldeliberately. - ID collisions after OOB swaps — duplicate
idattributes 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-indicatorand handle 4xx/5xx withhtmx:responseErrorlisteners. - 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.jsvia 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-targetandhx-swapper interaction; document non-obvious triggers. - Add indicators for requests expected to exceed 200 ms.
- Use
hx-confirmfor destructive actions (delete row, cancel order). - Configure
Content-Security-Policyto 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, andhx-triggercover the majority of dashboard and form interactions.hx-boostand OOB swaps deliver SPA-like UX while keeping routing on the server.- The
HX-Requestheader 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
- HTML fundamentals explained — semantic structure HTMX patches into
- Django fundamentals explained — templates and views that pair naturally with HTMX
- SSR, CSR, SSG, and ISR explained — where hypermedia fits in the rendering landscape
- WebSockets and SSE explained — real-time patterns beyond polling