# htmx User Guide

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.

```html
<!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"

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

=== "jsDelivr (minified)"

    ```html
    <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:

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

### 2.3 Via npm

The package is published as `htmx.org`.

```bash
npm install htmx.org@2.0.10
```

```text
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:

```javascript
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:

```text
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](#11-events-and-the-javascript-api)).

### 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']`.

```html
<!-- 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.

```html
<!-- 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 |

```html
<!-- Poll a status endpoint every 2 seconds -->
<div hx-get="/job/status" hx-trigger="every 2s" hx-target="this">
  Checking…
</div>
```

```html
<!-- 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.

```html
<!-- 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 |

```html
<!-- 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) |

```html
<!-- 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](#9-progressive-enhancement-hx-boost)) 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` |

```html
<!-- 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.

```html
<!-- 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 |

```html
<!-- 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.

```html
<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.

```html
<!-- 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 |

```html
<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.

```html
<!-- 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.

```html
<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`.

```html
<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.

```html
<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) |

```html
<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.

```javascript
// 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 `CustomEvent`s 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 |

```javascript
// 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.

```html
<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.

```html
<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 |

```html
<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.

```html
<!-- 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).

```python
# 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

### 16.1 Live search

```html
<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

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

### 16.3 Click-to-edit

```html
<!-- 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

```html
<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

```html
<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).

---

## 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`.

```html
<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 |

---

_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. Unofficial; not affiliated with or reviewed by the project._
