Jesse A. Green / notebook

htmx User Guide

Generated June 2026 from bigskysoftware/htmx at 5e867de

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

htmx is a small, dependency-free JavaScript library that lets you access modern browser features — AJAX requests, CSS transitions, WebSockets, and Server-Sent Events — directly from HTML attributes, without writing JavaScript. Instead of returning JSON and rendering it on the client, your server returns HTML, and htmx swaps that HTML into the page. This is the hypermedia approach: HTML stays the engine of application state.

This guide is for web developers who want to add dynamic, interactive behavior to their pages using htmx. It covers installation, every hx-* attribute, request triggering, content swapping, forms, browser history, and troubleshooting — all written from the htmx source at version 2.0.10.

Note

htmx works with any server-side technology that can return HTML — Django, Rails, Flask, Express, Go, PHP, ASP.NET, or a static file server. htmx does not care what generates the HTML; it only cares that the response is HTML.


1. Quick Start

Add htmx to your page with a single <script> tag, then annotate any element with hx-* attributes.

<!DOCTYPE html>
<html lang="en">
<head>
  <script src="https://unpkg.com/htmx.org@2.0.10"></script>
</head>
<body>
  <!-- When clicked, issue a POST to /clicked and replace this button
       with the HTML returned by the server -->
  <button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
  </button>
</body>
</html>

When the button is clicked, htmx:

  1. Sends an HTTP POST request to /clicked.
  2. Receives an HTML fragment in the response.
  3. Replaces the entire <button> element (outerHTML) with that fragment.

That is the whole model. The four ideas you need are: what triggers a request (hx-trigger), where it goes (hx-get/hx-post/...), where the response lands (hx-target), and how it is inserted (hx-swap).

Tip

htmx defaults are deliberately sensible. A <button hx-post="/x"> triggers on click, swaps into its own innerHTML, and targets itself — so you often only need one attribute.


2. Installation

2.1 Via CDN (fastest)

Drop one of these into your <head>. The unpkg and jsdelivr CDNs both serve htmx from the npm package.

unpkg

<script src="https://unpkg.com/htmx.org@2.0.10"></script>

jsDelivr (minified)

<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>

Caution

For production, pin an exact version (as shown above) rather than using a floating tag. Loading a third-party CDN means trusting that host; for stricter supply-chain control, self-host the file or add a Subresource Integrity (integrity) hash.

2.2 Self-hosting

Download htmx.min.js and serve it from your own origin:

<script src="/js/htmx.min.js"></script>

2.3 Via npm

The package is published as htmx.org.

npm install htmx.org@2.0.10
added 1 package in 1s

The npm package ships several entry points (declared in package.json):

Field File Use when
main dist/htmx.esm.js Bundling with a module system
unpkg / jsdelivr dist/htmx.min.js CDN <script> include
types dist/htmx.esm.d.ts TypeScript projects (type definitions)

To use it as an ES module in a bundler:

import htmx from 'htmx.org'
// htmx is now available as the default export
htmx.config.defaultSwapStyle = 'outerHTML'

Important

When you import htmx as a module, the global window.htmx may not be set automatically depending on your bundler. If your inline hx-* attributes stop working after switching to a bundled import, assign it manually: window.htmx = htmx.

2.4 Requirements

Requirement Detail
Runtime Any modern evergreen browser (Chrome, Firefox, Safari, Edge). htmx uses fetch-era browser APIs such as DOMParser, XMLHttpRequest, and the History API.
Server Anything that returns HTML over HTTP
Build step None. htmx is a single file with zero runtime dependencies.
License 0BSD (BSD Zero Clause — public-domain-equivalent, no attribution required)

3. Core Concepts

htmx adds an hx-* attribute vocabulary to HTML. Every attribute is inherited by descendant elements unless overridden, which means you can set behavior on a parent and have all children adopt it.

3.1 The request lifecycle

When a triggering event fires on an element with an hx-get/hx-post/etc. attribute, htmx runs this pipeline:

trigger event fires
  → htmx:confirm        (optional hx-confirm dialog)
  → htmx:configRequest  (headers + params assembled)
  → htmx:beforeRequest
  → AJAX request sent   (htmx-request class added to element)
  → response received
  → htmx:beforeSwap
  → content swapped into target   (htmx-swapping class on target)
  → htmx:afterSwap
  → settle phase        (htmx-settling class; attributes settle)
  → htmx:afterSettle
  → htmx:afterRequest   (htmx-request class removed)

You can hook into any of these stages with events (see Section 11).

3.2 The five request attributes

These attributes both enable an AJAX request and set its HTTP method. The attribute value is the URL (path).

Attribute HTTP method Notes
hx-get GET Parameters encoded in the URL query string
hx-post POST Parameters encoded in the request body
hx-put PUT Body-encoded
hx-patch PATCH Body-encoded
hx-delete DELETE Parameters encoded in the URL query string

Note

By default, GET and DELETE put parameters in the URL while POST, PUT, and PATCH put them in the body. This is controlled by htmx.config.methodsThatUseUrlParams, which defaults to ['get', 'delete'].

<!-- GET request, response replaces the div's contents -->
<div hx-get="/news/latest">Load latest news</div>

<!-- DELETE request triggered by a button -->
<button hx-delete="/items/42" hx-target="#item-42" hx-swap="delete">
  Delete item
</button>

4. Triggering Requests — hx-trigger

By default, htmx chooses a trigger event based on the element type:

Element Default trigger event
<form> submit
<input type="button">, <input type="submit"> click
<input>, <textarea>, <select> change
Everything else click

Override the default with hx-trigger, which accepts a comma-separated list of triggers. Each trigger is an event name optionally followed by modifiers.

<!-- Fire on key release in the search box -->
<input type="text" name="q"
       hx-get="/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results">

4.1 Trigger modifiers

These modifiers are parsed directly by htmx's trigger spec parser. Append them after the event name, separated by spaces:

Modifier Effect
once Trigger the request at most one time
changed Only fire if the element's value has changed since the last event
delay:<time> Wait <time> after the event before sending; a new event resets the timer (debounce)
throttle:<time> Fire at most once per <time> window; intermediate events are dropped
from:<selector> Listen for the event on a different element (e.g. from:body for global keyboard shortcuts)
target:<selector> Only fire if the event originated from an element matching <selector> (event delegation)
consume Stop the event from bubbling to ancestor htmx elements
queue:<option> Control how events that arrive during an in-flight request are queued (first, last, all, none)
root:<selector> (for intersect) the root element for intersection observation
threshold:<value> (for intersect) the intersection ratio that triggers (0.0–1.0)

<time> values use htmx's interval format: a number followed by ms (milliseconds), s (seconds), or m (minutes). A bare number is treated as milliseconds via parseFloat. Examples: delay:500ms, throttle:2s, delay:1m.

Tip

delay and throttle solve different problems. Use delay for "wait until the user stops typing" (search-as-you-type). Use throttle for "don't hammer the server more than once every N seconds" (a rapidly-clicked button).

4.2 Special trigger events

Trigger Meaning
load Fires once when the element is loaded/processed into the DOM (lazy-loading content)
revealed Fires when the element scrolls into the viewport
intersect Fires when the element intersects the viewport, using the Intersection Observer API (supports root: and threshold:)
every <time> Polling — re-issues the request on a fixed interval
<!-- Poll a status endpoint every 2 seconds -->
<div hx-get="/job/status" hx-trigger="every 2s" hx-target="this">
  Checking…
</div>
<!-- Lazy-load a heavy widget only when it scrolls into view -->
<div hx-get="/widgets/expensive" hx-trigger="revealed">
  <span class="htmx-indicator">Loading…</span>
</div>

Note

To stop polling from the server, respond with HTTP status 286. htmx treats 286 as a signal to cancel the polling loop for that element.

4.3 Event filters

You can guard a trigger with a JavaScript expression in square brackets. The request only fires when the expression evaluates truthy.

<!-- Only fire on Ctrl+click -->
<div hx-get="/info" hx-trigger="click[ctrlKey]">Ctrl-click me</div>

Warning

Event filters are evaluated with eval-like functionality. If you set htmx.config.allowEval = false (for a strict Content Security Policy), event filters, hx-vals with js:, and inline trigger conditions will stop working. See the Admin Guide for CSP guidance.


5. Targeting — hx-target

By default, the response is swapped into the element that triggered the request. Use hx-target to send it somewhere else. The value is a CSS selector, optionally using one of htmx's extended selectors.

Target value Resolves to
A CSS selector (e.g. #results, .panel) The first matching element
this The element with the hx-target attribute itself
closest <selector> Nearest ancestor (or self) matching the selector
find <selector> First descendant matching the selector
next / next <selector> The next sibling, or next matching element in the document
previous / previous <selector> The previous sibling, or previous matching element
window, document, body, root, host The corresponding node
<!-- Result goes into a sibling, not the button -->
<button hx-get="/profile" hx-target="#profile-panel">View profile</button>
<div id="profile-panel"></div>

<!-- Update the closest table row -->
<button hx-delete="/rows/7" hx-target="closest tr" hx-swap="outerHTML">
  Remove row
</button>

Tip

The closest, next, and previous selectors are the workhorses of reusable components — they let a fragment reference its surroundings without hard-coded IDs, so you can paste the same markup anywhere.


6. Swapping Content — hx-swap

hx-swap controls how the returned HTML is placed relative to the target. The first token is the swap style; additional space-separated tokens are modifiers.

6.1 Swap styles

Style Behavior
innerHTML Replace the target's inner content (default)
outerHTML Replace the entire target element
textContent Replace text content without parsing the response as HTML
beforebegin Insert before the target element
afterbegin Insert as the target's first child
beforeend Insert as the target's last child
afterend Insert after the target element
delete Delete the target element (response content is ignored)
none Do not append content (side effects like OOB swaps and triggers still run)
<!-- Append new messages to the bottom of a list -->
<div id="messages" hx-get="/messages" hx-trigger="every 5s" hx-swap="beforeend">
</div>

Note

The default swap style is htmx.config.defaultSwapStyle, which is innerHTML. Boosted elements (see Section 9) implicitly use innerHTML regardless of the global default.

6.2 Swap modifiers

Modifier Effect Example
swap:<time> Delay between receiving the response and swapping (lets you animate the old content out) swap:200ms
settle:<time> Delay between the swap and the "settle" phase (default 20ms) settle:100ms
transition:true Use the browser View Transitions API for this swap transition:true
scroll:<top|bottom|N> Scroll the target (or scroll:<selector>:top) after swapping scroll:bottom
show:<top|bottom> Scroll the target into view (show:<selector>:top) show:top
focus-scroll:<bool> Whether to scroll a newly-focused input into view focus-scroll:false
ignoreTitle:true Do not update the document <title> even if the response contains one ignoreTitle:true
<!-- Animate out (200ms), then swap, and scroll to the bottom -->
<div hx-get="/feed" hx-swap="innerHTML swap:200ms scroll:bottom">…</div>

6.3 Out-of-band swaps — hx-swap-oob

An element in the response marked with hx-swap-oob="true" is swapped into the matching element (by id) anywhere in the document, in addition to the main swap. This lets one response update multiple, unrelated parts of the page.

<!-- Server response -->
<div id="message">Saved!</div>
<div id="cart-count" hx-swap-oob="true">3 items</div>

The #message div goes into the request's normal target; the #cart-count div is swapped out-of-band into the existing #cart-count. You can also specify a swap style: hx-swap-oob="beforeend:#log".

Note

Whether OOB swaps are processed on elements nested inside the main response is governed by htmx.config.allowNestedOobSwaps (default true).


7. Selecting Part of a Response — hx-select and hx-select-oob

When your server returns a full page but you only want one piece of it, use hx-select to pick the fragment to swap in.

Attribute Effect
hx-select A CSS selector that picks the content to swap from the response
hx-select-oob A comma-separated list of selectors to swap out-of-band from the response
<!-- The server returns a whole page; we only want #content from it -->
<a hx-get="/page/2" hx-select="#content" hx-target="#content">Next page</a>

Tip

hx-select pairs naturally with hx-boost (Section 9): you can boost normal links and forms while pulling only the meaningful region out of each full-page response.


8. Sending Data with Requests

8.1 Form values

When the triggering element is (or is inside) a <form>, htmx automatically includes all the form's input values. For non-form elements, htmx includes the element's own value and that of any enclosing form for non-GET requests.

<form hx-post="/account" hx-target="#result">
  <input name="email" type="email">
  <input name="age" type="number">
  <button type="submit">Save</button>
</form>

8.2 Including extra inputs — hx-include

Pull in values from elements outside the triggering element using a CSS selector.

<!-- Include the value of #filter even though it's elsewhere on the page -->
<button hx-get="/search" hx-include="#filter" hx-target="#results">Search</button>
<input id="filter" name="filter" type="text">

8.3 Filtering parameters — hx-params

Control which parameters are submitted.

Value Meaning
* Include all parameters (default)
none Include no parameters
not <names> Include all except the comma-separated names
<names> Include only the comma-separated names
<form hx-post="/save" hx-params="not csrf_debug">…</form>

8.4 Adding values — hx-vals and hx-vars

hx-vals adds name/value pairs to the request as JSON. Prefix the value with js: (or javascript:) to evaluate it dynamically.

<!-- Static extra values -->
<div hx-get="/items" hx-vals='{"category": "books", "sort": "title"}'>…</div>

<!-- Dynamic value computed at request time -->
<div hx-get="/items" hx-vals='js:{timestamp: Date.now()}'>…</div>

Important

hx-vars is the older, always-evaluated form of hx-vals and is retained for compatibility. Prefer hx-vals — it is safer because static JSON is not passed through eval, and the js: prefix makes dynamic evaluation explicit.

8.5 Custom headers — hx-headers

Add request headers as JSON. Supports the same js: dynamic-evaluation prefix.

<div hx-get="/api/data" hx-headers='{"X-Custom-Header": "value"}'>…</div>

8.6 File uploads — hx-encoding

To upload files, set the encoding to multipart/form-data.

<form hx-post="/upload" hx-encoding="multipart/form-data">
  <input type="file" name="document">
  <button type="submit">Upload</button>
</form>

9. Progressive Enhancement — hx-boost

hx-boost="true" converts ordinary <a> links and <form> submissions into AJAX requests that swap the page <body> and update the URL — giving you SPA-like navigation while keeping the page fully functional if JavaScript is disabled.

<body hx-boost="true">
  <nav>
    <a href="/">Home</a>      <!-- now AJAX, no full reload -->
    <a href="/about">About</a>
  </nav>
</body>

Because hx-boost is inherited, setting it on <body> boosts every link and form on the page. Opt individual elements out with hx-boost="false".

Note

Boosted requests only apply to same-origin links (location.hostname must match) and forms whose method is not dialog. Links with a target other than _self, and Ctrl/Cmd-clicks (open-in-new-tab), fall through to normal browser behavior. Whether a boosted target is scrolled into view is controlled by htmx.config.scrollIntoViewOnBoost (default true).


10. Browser History — hx-push-url and hx-replace-url

htmx can manage the browser address bar and back/forward navigation.

Attribute Effect
hx-push-url="true" Push the request URL into the browser history (creates a new back-button entry)
hx-push-url="/custom/path" Push a specific URL instead of the request URL
hx-replace-url="true" Replace the current history entry (no new back-button entry)
hx-history-elt Marks the element htmx snapshots for history restoration (defaults to <body>)
hx-history="false" Excludes a page from the history snapshot cache (e.g. pages with sensitive data)
<a hx-get="/articles/5" hx-target="#main" hx-push-url="true">Read article</a>

When the user presses Back, htmx restores the snapshot from its history cache. The cache size is htmx.config.historyCacheSize (default 10 pages, stored in sessionStorage).

Caution

Set hx-history="false" on any page or fragment that should not be written to sessionStorage — for example, content containing personal or financial data. The history cache lives in the browser and is readable by client-side scripts.


11. Events and the JavaScript API

You rarely need JavaScript with htmx, but when you do, the htmx object exposes a clean API and a rich event system. Every value below is taken from the library's public API definition.

11.1 Common methods

Method Purpose
htmx.ajax(verb, path, target) Issue an htmx request from script; returns a Promise
htmx.process(elt) Initialize htmx attributes on dynamically-added content
htmx.on(target, event, listener) Add an event listener (shorthand for addEventListener)
htmx.off(target, event, listener) Remove an event listener
htmx.trigger(elt, eventName, detail) Dispatch a custom event on an element
htmx.find(selector) / htmx.findAll(selector) Query helpers
htmx.closest(elt, selector) Nearest matching ancestor
htmx.values(elt, verb) Resolve the input values that would be submitted
htmx.swap(target, content, swapSpec) Perform a swap manually
htmx.addClass / removeClass / toggleClass / takeClass Class helpers (with optional delay)
htmx.remove(elt, delay) Remove an element (optionally after a delay)
htmx.defineExtension(name, ext) Register an extension
htmx.logAll() / htmx.logNone() Enable/disable verbose event logging
htmx.parseInterval(str) Parse an htmx interval string (e.g. "2s") into milliseconds

Important

When you insert HTML into the page yourself (outside of an htmx swap), htmx does not know about it. Call htmx.process(newElement) so its hx-* attributes become active.

// Lazily trigger a request from code
htmx.ajax('GET', '/rows', '#table-body')

// React to every completed request
htmx.on('htmx:afterRequest', function (evt) {
  console.log('Request to', evt.detail.pathInfo.requestPath, 'finished')
})

11.2 Key events

htmx fires CustomEvents at every stage. The most useful ones:

Event When it fires Common use
htmx:load New content is loaded and processed Initialize third-party JS widgets
htmx:configRequest Before the request is sent Add headers/params from script
htmx:beforeRequest Just before sending Cancel the request (evt.preventDefault())
htmx:beforeSwap Before content is swapped Override target, content, or whether to swap
htmx:afterSwap After content is swapped Post-swap DOM work
htmx:afterSettle After the settle phase Run animations
htmx:afterRequest After the request completes Logging, cleanup
htmx:responseError Server returned a 4xx/5xx Show an error message
htmx:sendError Network error (no response) Show an offline notice
htmx:timeout Request exceeded the timeout Retry / notify
htmx:confirm An hx-confirm is being evaluated Custom confirmation UI
// Add a CSRF token to every request
document.body.addEventListener('htmx:configRequest', function (evt) {
  evt.detail.headers['X-CSRF-Token'] = window.csrfToken
})

Tip

Run htmx.logAll() in the browser console during development. It logs every htmx event with its element and detail payload — the fastest way to understand exactly what htmx is doing on your page.


12. User Feedback

12.1 Request indicators — hx-indicator

While a request is in flight, htmx adds the htmx-request class to the triggering element. Any element with the htmx-indicator class becomes visible during this window. Use hx-indicator to point at a specific spinner.

<button hx-get="/slow" hx-indicator="#spinner">Load</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif">

By default htmx injects a small CSS rule so that .htmx-indicator elements are hidden (opacity 0) until a request is active. This injection is controlled by htmx.config.includeIndicatorStyles (default true).

12.2 Disabling elements during a request — hx-disabled-elt

Disable elements (e.g. the submit button) while a request runs, to prevent double-submits.

<button hx-post="/order" hx-disabled-elt="this">Place order</button>

12.3 CSS classes htmx manages

Class Applied to When
htmx-request Triggering element During an in-flight request
htmx-indicator Indicator elements Authored by you; revealed during requests
htmx-swapping Target During the swap (between content out and in)
htmx-settling Target During the settle phase
htmx-added New nodes Briefly, right after insertion

Tip

Use the htmx-swapping and htmx-settling classes plus a swap:/settle: delay to build smooth fade transitions in pure CSS — no JavaScript animation library required.


13. Confirmations and Prompts

Attribute Effect
hx-confirm="message" Show a confirmation dialog before issuing the request
hx-prompt="message" Show a prompt; the user's input is sent in the HX-Prompt request header
<button hx-delete="/account" hx-confirm="Delete your account permanently?">
  Delete account
</button>

14. Synchronizing Requests — hx-sync

When multiple elements can issue requests that would conflict, hx-sync coordinates them against a common element.

<!-- Abort the in-flight search if a new keystroke arrives -->
<input name="q" hx-get="/search" hx-trigger="keyup changed delay:200ms"
       hx-sync="this:replace">

Common strategies: drop (ignore the new request if one is in flight), abort (cancel the in-flight one), replace (cancel and replace), and queue variants.


15. Server Response Headers

Your server can steer htmx from the response side using HX-* headers. These give the server fine control without changing the client markup.

Response header Effect
HX-Location Client-side redirect that does an htmx swap (no full reload)
HX-Redirect Full-page client-side redirect to a new URL
HX-Refresh: true Force a full page reload
HX-Push-Url Push a URL into history
HX-Replace-Url Replace the current URL in history
HX-Retarget Override the target element (a CSS selector)
HX-Reswap Override the hx-swap behavior
HX-Reselect Override hx-select for this response
HX-Trigger Trigger client-side events after receiving the response
HX-Trigger-After-Swap Trigger events after the swap
HX-Trigger-After-Settle Trigger events after the settle phase

htmx also sends request headers your server can read: HX-Request: true (present on every htmx request), HX-Trigger (id of the triggering element), HX-Trigger-Name (its name), HX-Target (id of the target), HX-Current-URL, HX-Prompt (any hx-prompt input), and HX-Boosted (present on boosted requests).

# Example: a Flask handler returning a partial only for htmx requests
@app.post("/save")
def save():
    do_save()
    resp = make_response(render_template("_row.html"))
    resp.headers["HX-Trigger"] = "itemSaved"   # fire a client event
    return resp

Tip

Check for the HX-Request header on the server to decide whether to return a full page (normal navigation) or just a fragment (htmx request). This single conditional is the backbone of a progressively-enhanced htmx app.


16. Common Recipes

<input type="search" name="q"
       hx-get="/search"
       hx-trigger="input changed delay:300ms, search"
       hx-target="#search-results"
       hx-indicator="#search-spinner">
<span id="search-spinner" class="htmx-indicator">Searching…</span>
<div id="search-results"></div>

16.2 Infinite scroll

<tr hx-get="/contacts?page=2"
    hx-trigger="revealed"
    hx-swap="afterend">
  <td>Last visible row…</td>
</tr>

16.3 Click-to-edit

<!-- View mode; clicking swaps in the edit form returned by the server -->
<div hx-get="/contact/1/edit" hx-trigger="click" hx-swap="outerHTML">
  <strong>Joe</strong> — click to edit
</div>

16.4 Delete a table row

<button hx-delete="/contact/1"
        hx-confirm="Are you sure?"
        hx-target="closest tr"
        hx-swap="outerHTML swap:500ms">
  Delete
</button>

16.5 Polling for a background job

<div hx-get="/jobs/abc/status" hx-trigger="every 1s" hx-target="this" hx-swap="innerHTML">
  Working…
</div>
<!-- When the job is done, the server returns the final HTML and a 286 status
     to stop the poll -->

17. Troubleshooting

Error: The selector "<selector>" on hx-target returned no matches!

Cause: The CSS selector in hx-target (or hx-include) does not match any element in the DOM at request time.

Solution: Verify the target element exists and the selector is correct. Remember that extended selectors (closest, next, previous) are relative to the triggering element. If the target is added dynamically, ensure it exists before the trigger fires.

Prevention: Prefer closest/find/next/previous for content that moves, and reserve #id selectors for stable page landmarks.

Error: htmx:syntax:error in the console

Cause: htmx could not parse an hx-trigger value — usually an unknown modifier or a malformed event filter expression.

Solution: Re-check the hx-trigger syntax. Modifiers like delay, throttle, from, target, throttle, queue, root, and threshold require a : and a value (e.g. delay:200ms). Event filter expressions go in square brackets: click[shiftKey].

Prevention: Keep complex trigger specs readable; test them with htmx.logAll() enabled.

Issue: My dynamically-inserted HTML ignores its hx-* attributes

Cause: htmx only processes content it inserts via a swap. HTML you add with raw DOM APIs or another library is invisible to htmx.

Solution: Call htmx.process(element) on the newly-added node.

Prevention: Insert dynamic content through htmx swaps (including OOB swaps) wherever possible.

Issue: The page title isn't updating / is updating unexpectedly

Cause: htmx updates document.title when a response contains a <title>. The global htmx.config.ignoreTitle (default false) and the per-swap ignoreTitle:true modifier control this.

Solution: Add hx-swap="... ignoreTitle:true" to suppress title updates for a specific swap, or set htmx.config.ignoreTitle = true globally.

Issue: Cross-origin requests are blocked

Cause: htmx.config.selfRequestsOnly defaults to true, which blocks htmx requests to other origins as a security measure.

Solution: Only relax this if you genuinely need cross-origin requests, by setting htmx.config.selfRequestsOnly = false. Understand the security implications first (see Admin Guide).

Issue: A request fires but nothing changes in the DOM

Cause: The server returned a 204 No Content, or a 4xx/5xx status. By htmx's default response handling, 204 does not swap, 2xx/3xx swap, and 4xx/5xx are treated as errors and do not swap.

Solution: Listen for htmx:responseError to surface server errors. To swap error responses, customize htmx.config.responseHandling or use the HX-Reswap response header.

Issue: WebSocket / SSE attributes don't work

Cause: WebSocket and Server-Sent Events support moved into extensions in htmx 2 and are no longer part of the core library.

Solution: Include the ws or sse extension script and register it with hx-ext. See Section 18.


18. Extensions

htmx 2 keeps the core minimal and moves optional features into extensions. Load an extension's script, then activate it on an element (and its children, via inheritance) with hx-ext.

<script src="https://unpkg.com/htmx-ext-ws@2.0.0"></script>
<div hx-ext="ws" ws-connect="/chatroom">
  …
</div>

Extensions can transform responses, handle custom swap styles, encode parameters, and react to events. You can write your own with htmx.defineExtension(name, { ... }).


19. FAQ

Q: Do I need a build step or framework to use htmx? No. htmx is a single JavaScript file with zero dependencies. Add one <script> tag and you are done.

Q: What does the server need to return? HTML — typically a fragment (the piece to swap in), not JSON. Any server technology that emits HTML works.

Q: How is htmx different from a SPA framework like React? htmx keeps application state and rendering on the server. You send HTML over the wire and swap it into the page, rather than maintaining a client-side virtual DOM and serializing state as JSON. This drastically reduces client-side JavaScript for many CRUD-style applications.

Q: Can I use htmx alongside React/Vue/Alpine? Yes. htmx is commonly paired with Alpine.js or hyperscript for local interactivity. Use htmx:load to initialize other libraries on swapped-in content.

Q: How do I add a CSRF token to every request? Listen for htmx:configRequest and set evt.detail.headers['X-CSRF-Token'], or put it in an inherited hx-headers on <body>.

Q: How do I stop a polling request? Return HTTP status 286 from the polled endpoint, or remove/replace the polling element.

Q: Why is my response being treated as an error? htmx's default response handling treats 4xx and 5xx as errors that do not swap. Use htmx:responseError, or override with HX-Reswap/htmx.config.responseHandling.

Q: Does htmx work if JavaScript is disabled? The htmx behaviors themselves require JavaScript. However, if you use hx-boost on normal <a>/<form> elements, the page degrades gracefully to standard full-page navigation when JS is off.

Q: How do I configure htmx globally? Set properties on htmx.config in a script after htmx loads, or use a <meta name="htmx-config" content='{...}'> tag in the document head.

Q: Is htmx accessible? htmx swaps real HTML, so server-rendered semantic markup and ARIA attributes are preserved. Because content changes dynamically, use ARIA live regions and manage focus for screen-reader users — htmx does not do this automatically.

Q: What's the difference between hx-vals and hx-vars? Both add extra request parameters. hx-vars always evaluates its value as JavaScript; hx-vals takes JSON by default and only evaluates when prefixed with js:. Prefer hx-vals.

Q: How do I update several parts of the page from one response? Use out-of-band swaps (hx-swap-oob="true" on elements in the response) or hx-select-oob.


20. Attribute Quick Reference

Attribute Purpose
hx-get / hx-post / hx-put / hx-patch / hx-delete Issue an AJAX request with the given HTTP method
hx-trigger The event(s) that trigger the request, plus modifiers
hx-target Where the response is swapped (CSS / extended selector)
hx-swap How the response is swapped, plus modifiers
hx-swap-oob Mark response content for an out-of-band swap
hx-select Select a fragment from the response to swap
hx-select-oob Select fragments from the response for OOB swaps
hx-boost Convert links/forms to AJAX navigation
hx-push-url / hx-replace-url Manage the browser address bar and history
hx-history / hx-history-elt Control history snapshotting
hx-include Include extra inputs in the request
hx-params Filter which parameters are sent
hx-vals / hx-vars Add extra request parameters
hx-headers Add request headers
hx-encoding Set request encoding (e.g. multipart/form-data)
hx-indicator Element to show during a request
hx-disabled-elt Element(s) to disable during a request
hx-confirm Confirmation dialog before requesting
hx-prompt Prompt for input before requesting
hx-sync Synchronize/abort conflicting requests
hx-ext Activate an extension
hx-on:<event> Inline event handler for htmx (and DOM) events
hx-preserve Preserve an element unchanged across swaps
hx-disable Disable htmx processing on an element and its children
hx-validate Force HTML5 validation before a request
hx-inherit / hx-disinherit Control attribute inheritance to descendants