Install
One script tag, no build step:
<script src="https://unpkg.com/htmx.org@2.0.4"
integrity="sha384-..."
crossorigin="anonymous"></script>
Or download htmx.min.js and self-host. With Bun (see that tutorial) or npm, bun add htmx.org for ESM imports.
The first example
<button hx-get="/api/time"
hx-trigger="click"
hx-target="#time"
hx-swap="innerHTML">
Get current time
</button>
<div id="time"></div>
Click the button. HTMX issues a GET to /api/time, gets back HTML (not JSON), and swaps it into #time. The server might return:
<span>2026-04-30 14:32:17 EDT</span>
That's the whole model. Every interaction is "make an HTTP request, swap the response somewhere."
The core attributes
hx-get/hx-post/hx-put/hx-patch/hx-delete— issue an AJAX requesthx-target— CSS selector for what to update (default: the element itself)hx-swap— how to insert the response:innerHTML(default),outerHTML,beforebegin,afterbegin,beforeend,afterend,delete,nonehx-trigger— what fires the request:click(default for buttons/links),submit(default for forms),change,keyup changed delay:500ms,load,revealed,every 2shx-vals/hx-include— extra form values or include other inputs in the requesthx-confirm/hx-prompt— native confirm / prompt before the requesthx-indicator— show a CSS class while the request is in flighthx-push-url— push a URL into the browser history so back/forward work
A real form: inline search
<input type="search"
name="q"
placeholder="Search..."
hx-get="/users/search"
hx-trigger="keyup changed delay:300ms, search"
hx-target="#results"
hx-indicator="#spinner">
<img id="spinner" class="htmx-indicator" src="/spinner.svg">
<ul id="results"></ul>
The server's /users/search?q=... returns an HTML <li> list. HTMX swaps it into #results with a 300ms debounce. No fetch(), no JSON, no router, no state library.
Inline edit pattern
Display mode:
<div hx-target="this" hx-swap="outerHTML">
<span>Amir Eslampanah</span>
<button hx-get="/users/42/edit">Edit</button>
</div>
Edit mode (what /users/42/edit returns):
<form hx-put="/users/42" hx-target="this" hx-swap="outerHTML">
<input name="name" value="Amir Eslampanah">
<button type="submit">Save</button>
<button hx-get="/users/42">Cancel</button>
</form>
Click Edit → the div is replaced by the form. Save → the form is replaced by the updated display. Cancel → the form is replaced by the original display. No client state to manage.
Server-side: any framework that returns HTML
HTMX is back-end-agnostic. Pseudo-code in three popular shapes:
# Python (FastAPI + Jinja2)
@app.get("/users/search", response_class=HTMLResponse)
def search(q: str = ""):
users = db.search(q)
return templates.TemplateResponse("partials/user_list.html", { "users": users })
# Go (net/http + html/template)
func search(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
users := db.Search(q)
tmpl.ExecuteTemplate(w, "user_list.html", users)
}
# Ruby (Rails)
def search
@users = User.search(params[:q])
render partial: "user_list", locals: { users: @users }
end
The pattern: receive a request, render a fragment template, return it. No serializer step, no API versioning, no client-side router.
Form validation that doesn't suck
<form hx-post="/signup" hx-target="this" hx-swap="outerHTML">
<input name="email"
type="email"
hx-post="/signup/validate/email"
hx-trigger="change"
hx-target="next .error">
<span class="error"></span>
<input name="password" type="password">
<button type="submit">Sign up</button>
</form>
Per-field validation fires on change; the server returns <span class="error">Email already in use</span> or an empty span on success. Final submit re-validates everything.
Streaming updates: SSE
<div hx-ext="sse" sse-connect="/events">
<div sse-swap="notification" hx-swap="afterbegin">
<!-- new notifications appear here as they arrive -->
</div>
</div>
HTMX has an SSE extension (load htmx-ext-sse) that opens an EventSource to /events and swaps named events into the DOM. Same idea for WebSockets (hx-ext="ws" with ws-connect).
What HTMX doesn't replace
- Genuinely client-heavy apps — rich-text editors, video editors, 3D / WebGL, multiplayer games. The state is fundamentally on the client; HTMX has nothing to offer.
- Offline / PWA workflows — HTMX assumes the server is reachable. For offline-first apps, a real client-side framework is the right tool.
- Native mobile UI — HTMX requires a browser. For native mobile, the back-end stays the same but the front-end is a different beast.
For everything else — admin panels, dashboards, CRUD apps, content sites with logged-in areas, internal tools — HTMX makes the "do I really need React for this" answer be no. A two-line script tag plus a server that already renders HTML covers it.
Worth knowing
- Read the htmx.org/essays/ page (the project's manifesto-style writeups). They are unusually well-argued and worth the hour.
- The companion library
hyperscripthandles client-side behaviour that doesn't need a server round-trip (toggling classes, focusing elements). Optional, but a natural pair. - HTMX has a small set of official extensions: SSE, WebSockets, alpine-morph, JSON encoding, debug, response targets.
- For server-side template engines that are HTMX-aware out of the box, look at htmx-flask (Python), htmx-go libraries, and Hotwire-style helpers in Rails.