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 request
  • hx-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, none
  • hx-trigger — what fires the request: click (default for buttons/links), submit (default for forms), change, keyup changed delay:500ms, load, revealed, every 2s
  • hx-vals / hx-include — extra form values or include other inputs in the request
  • hx-confirm / hx-prompt — native confirm / prompt before the request
  • hx-indicator — show a CSS class while the request is in flight
  • hx-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 hyperscript handles 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.