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:
- Sends an HTTP
POSTrequest to/clicked. - Receives an HTML fragment in the response.
- 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.htmxmay not be set automatically depending on your bundler. If your inlinehx-*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,
GETandDELETEput parameters in the URL whilePOST,PUT, andPATCHput them in the body. This is controlled byhtmx.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
delayandthrottlesolve different problems. Usedelayfor "wait until the user stops typing" (search-as-you-type). Usethrottlefor "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 treats286as 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 sethtmx.config.allowEval = false(for a strict Content Security Policy), event filters,hx-valswithjs:, 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, andpreviousselectors 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 isinnerHTML. Boosted elements (see Section 9) implicitly useinnerHTMLregardless 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(defaulttrue).
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-selectpairs naturally withhx-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-varsis the older, always-evaluated form ofhx-valsand is retained for compatibility. Preferhx-vals— it is safer because static JSON is not passed througheval, and thejs: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.hostnamemust match) and forms whosemethodis notdialog. Links with a target other than_self, andCtrl/Cmd-clicks (open-in-new-tab), fall through to normal browser behavior. Whether a boosted target is scrolled into view is controlled byhtmx.config.scrollIntoViewOnBoost(defaulttrue).
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 tosessionStorage— 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 itshx-*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-swappingandhtmx-settlingclasses plus aswap:/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-Requestheader 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
16.1 Live search
<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 |