---
name: htmx-guidance
description: Use when writing HTML with htmx, building htmx-powered pages, or answering questions about htmx patterns and best practices. Covers htmx 4 attributes, events, swap strategies, and common UI patterns.
---

# htmx 4 Guidance

htmx allows any HTML element to issue HTTP requests and swap the response into the DOM.
The server returns **HTML fragments**, not JSON. This is the fundamental model.

htmx 4 uses the `fetch()` API (not XMLHttpRequest like htmx 2).

## Core Attributes

Issue requests with these attributes. Each takes a URL:

| Attribute   | Description    |
|-------------|----------------|
| `hx-get`    | GET request    |
| `hx-post`   | POST request   |
| `hx-put`    | PUT request    |
| `hx-patch`  | PATCH request  |
| `hx-delete` | DELETE request |

### Default Triggers

- `input`, `textarea`, `select` trigger on `change`
- `form` triggers on `submit`
- Everything else triggers on `click`

Override with `hx-trigger`.

## hx-trigger

Specify what event triggers the request:

```html

<div hx-get="/data" hx-trigger="mouseenter">Hover me</div>
```

**Modifiers:**

- `once` -- fire only once
- `changed` -- only if value changed
- `delay:<time>` -- debounce (resets on re-trigger), e.g. `delay:500ms`
- `throttle:<time>` -- throttle (ignores during cooldown)
- `from:<selector>` -- listen on a different element (supports `document`, `window`, `closest <sel>`, `find <sel>`,
  `next`, `previous`)
- `target:<selector>` -- filter by event target
- `consume` -- stop event from triggering other htmx requests on parents

**Filters** (JavaScript expressions in brackets):

```html

<div hx-get="/data" hx-trigger="click[ctrlKey]">Ctrl+Click me</div>
```

**Special events:**

- `load` -- fires when element is loaded
- `revealed` -- fires when element scrolls into viewport
- `intersect` -- fires on intersection (options: `root:<sel>`, `threshold:<float>`)

**Polling:**

```html

<div hx-get="/updates" hx-trigger="every 2s">Poll</div>
```

**Multiple triggers** (comma-separated):

```html
<input hx-get="/search" hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
       hx-target="#results">
```

**Triggering from HX-Trigger header** -- use `from:body`:

```html

<div hx-get="/table" hx-trigger="refreshTable from:body">...</div>
```

## hx-target

CSS selector for where the response content goes. Defaults to the element itself.

**Extended CSS selectors:**

- `this` -- the element with the attribute
- `closest <selector>` -- nearest ancestor matching selector
- `find <selector>` -- first child descendant matching selector
- `next [selector]` -- next sibling (optionally matching selector)
- `previous [selector]` -- previous sibling (optionally matching selector)

The relative selectors can be used to avoid adding ids to DOM elements, for example in a
table generated by a loop.

```html

<button hx-get="/data" hx-target="closest .container">Load</button>
```

## hx-swap

Controls how response content is placed relative to the target. Default: `innerHTML`.

| Value                    | Description                                        |
|--------------------------|----------------------------------------------------|
| `innerHTML`              | Replace inner HTML of target                       |
| `outerHTML`              | Replace entire target element                      |
| `innerMorph`             | Morph children of target (preserves DOM state)     |
| `outerMorph`             | Morph target itself (preserves DOM state)          |
| `textContent`            | Replace text content, no HTML parsing              |
| `before` / `beforebegin` | Insert before the target                           |
| `prepend` / `afterbegin` | Insert before target's first child                 |
| `append` / `beforeend`   | Insert after target's last child                   |
| `after` / `afterend`     | Insert after the target                            |
| `delete`                 | Delete the target regardless of response           |
| `none`                   | Don't swap (OOB swaps and headers still processed) |

**Modifiers** (space-separated after swap style):

```html

<div hx-get="/data" hx-swap="innerHTML swap:100ms settle:200ms transition:true ignoreTitle:true scroll:top">
```

| Modifier            | Description                                  |
|---------------------|----------------------------------------------|
| `swap:<time>`       | Delay before swap                            |
| `settle:<time>`     | Delay between swap and settle                |
| `transition:true`   | Use View Transitions API                     |
| `ignoreTitle:true`  | Don't update page title from response        |
| `scroll:top/bottom` | Scroll target after swap                     |
| `show:top/bottom`   | Scroll target into viewport                  |
| `strip:true`        | Remove outer wrapper element before swapping |
| `focusScroll:true`  | Scroll to focused element                    |
| `target:<selector>` | Retarget the swap                            |

## Attribute Inheritance (CRITICAL htmx 4 change)

**In htmx 4, inheritance is explicit by default.** Use the `:inherited` modifier on parent elements:

```html
<!-- WRONG in htmx 4: children won't inherit hx-target -->
<div hx-target="#output">
    <button hx-get="/a">A</button>
    <button hx-get="/b">B</button>
</div>

<!-- CORRECT: use :inherited modifier -->
<div hx-target:inherited="#output">
    <button hx-get="/a">A</button>
    <button hx-get="/b">B</button>
</div>
```

The `:append` modifier appends to inherited values:

```html

<div hx-include:inherited="[name='token']">
    <button hx-post="/save" hx-include:inherited:append="[name='extra']">Save</button>
</div>
```

To revert to implicit inheritance globally: set `htmx.config.implicitInheritance = true`.

## Configuration

Set via meta tag or JavaScript:

```html

<meta name="htmx-config" content='{"defaultSwap":"outerHTML"}'>
```

Key config values:

| Config                | Default                 | Description                                            |
|-----------------------|-------------------------|--------------------------------------------------------|
| `defaultSwap`         | `innerHTML`             | Default swap strategy                                  |
| `defaultTimeout`      | `60000`                 | Request timeout (ms)                                   |
| `noSwap`              | `[204, 304]`            | Status codes that skip swapping                        |
| `implicitInheritance` | `false`                 | Auto-inherit attributes from parents                   |
| `transitions`         | `false`                 | Enable View Transitions globally                       |
| `logAll`              | `false`                 | Log all events to console (debugging)                  |
| `mode`                | `same-origin`           | Fetch mode (`cors`, `no-cors`, `same-origin`)          |
| `history`             | `true`                  | Enable history support (`true`, `false`, `"reload"`)   |
| `extensions`          | `""`                    | Whitelist of allowed extensions (empty = allow all)    |
| `prefix`              | `""`                    | Custom attribute prefix (e.g. `"data-hx-"`)            |
| `morphIgnore`         | `["data-htmx-powered"]` | Attribute name prefixes to leave unchanged when morphing |
| `morphScanLimit`      | `10`                    | Sibling scan limit during morphing                       |
| `morphSkip`           | `'[hx-morph-skip]'`     | CSS selector for elements to skip morphing entirely      |
| `morphSkipChildren`   | `'[hx-morph-skip-children]'` | CSS selector for elements whose children skip morphing |

## Events

htmx 4 naming convention: `htmx:phase:action`

**Lifecycle:**

- `htmx:before:init` / `htmx:after:init` -- element initialization
- `htmx:before:cleanup` / `htmx:after:cleanup` -- element removal

**Request:**

- `htmx:config:request` -- configure request (modify headers, body, URL). Cancel with `evt.preventDefault()`
- `htmx:before:request` -- just before fetch. Cancel with `evt.preventDefault()`
- `htmx:before:response` -- after fetch response received, before body consumed
- `htmx:after:request` -- after request completes
- `htmx:finally:request` -- when request completes, fails, or is cancelled
- `htmx:error` -- on any error (network, response, swap)

**Swap:**

- `htmx:before:swap` / `htmx:after:swap` -- before/after content swap
- `htmx:before:settle` / `htmx:after:settle` -- before/after settle phase
- `htmx:confirm` -- after trigger, before request (for async confirmation)

**History:**

- `htmx:before:history:update` / `htmx:after:history:update`
- `htmx:after:history:push` / `htmx:after:history:replace`
- `htmx:before:history:restore`

**View Transitions:**

- `htmx:before:viewTransition` / `htmx:after:viewTransition`

### Request Context

Events expose `detail.ctx` with the full request context:

```js
document.body.addEventListener('htmx:config:request', (evt) => {
    let ctx = evt.detail.ctx;
    // ctx.sourceElement  -- element that triggered request
    // ctx.target         -- swap target element
    // ctx.swap           -- hx-swap value
    // ctx.request.action -- URL
    // ctx.request.method -- HTTP method
    // ctx.request.headers -- headers object
    // ctx.request.body   -- FormData body
});
```

### Inline Event Handlers

Use `hx-on:event-name` for inline handlers:

```html

<button hx-get="/data" hx-on:htmx:after:swap="alert('Swapped!')">Load</button>
```

## HTTP Headers

### Request Headers (sent by htmx)

| Header                       | Description                                                                     |
|------------------------------|---------------------------------------------------------------------------------|
| `HX-Request`                 | Always `"true"` for htmx requests                                               |
| `HX-Source`                  | Triggering element as `tag#id` (e.g. `button#submit`)                           |
| `HX-Target`                  | Target element as `tag#id` (e.g. `div#results`)                                 |
| `HX-Current-URL`             | Browser's current URL                                                           |
| `HX-Request-Type`            | `"partial"` for targeted swaps, `"full"` when targeting body or using hx-select |
| `HX-Boosted`                 | `"true"` if via hx-boost                                                        |
| `HX-History-Restore-Request` | `"true"` if restoring history                                                   |

### Response Headers (server sends to htmx)

| Header           | Description                                      |
|------------------|--------------------------------------------------|
| `HX-Trigger`     | Trigger client-side events (single name or JSON) |
| `HX-Push-Url`    | Push URL to browser history                      |
| `HX-Replace-Url` | Replace current URL in history                   |
| `HX-Redirect`    | Client-side redirect (full page)                 |
| `HX-Location`    | Client-side redirect via AJAX (no full reload)   |
| `HX-Refresh`     | Full page refresh if `"true"`                    |
| `HX-Retarget`    | Override target with CSS selector                |
| `HX-Reswap`      | Override swap strategy                           |
| `HX-Reselect`    | Override hx-select                               |

## Status-Based Response Handling (hx-status)

Handle different HTTP status codes with different swap behavior:

```html

<form hx-post="/register"
      hx-target="#result"
      hx-status:422="target:#errors select:#validation-errors"
      hx-status:5xx="swap:none">
    <input name="email" type="email">
    <div id="errors"></div>
    <div id="result"></div>
    <button type="submit">Register</button>
</form>
```

Supports wildcards: `hx-status:5xx`, `hx-status:50x`, `hx-status:404`.

Config options in the value: `swap:`, `target:`, `select:`, `push:`, `replace:`, `transition:`.

## Updating Multiple Page Regions

Three main approaches:

### 1. Expand the Target

Wrap both regions in a container and target it:

```html

<div id="page-section">
    <div id="table">...</div>
    <form hx-post="/contacts" hx-target="#page-section">...</form>
</div>
```

Server returns both the table and the form.

### 2. Out-of-Band Swaps

Server response includes extra elements with `hx-swap-oob`:

```html
<!-- Main response content (swapped into target normally) -->
<form>...</form>

<!-- This gets swapped into #contacts-table by ID -->
<tbody hx-swap-oob="beforeend:#contacts-table">
<tr>
    <td>New row</td>
</tr>
</tbody>
```

Note: in htmx 4, OOB swaps happen AFTER the main content swap.

### 3. Partial Tags

New in htmx 4, a more general version of OOB swaps

```html

<hx-partial hx-target="#messages" hx-swap="beforeend">
    <div>New message</div>
</hx-partial>

<hx-partial hx-target="#notifications" hx-swap="innerHTML">
    <span class="badge">5</span>
</hx-partial>
```

Each `<hx-partial>` specifies its own target and swap strategy. Preferred over OOB for explicit targeting.

### 4. Event-Driven Refresh

Server sends `HX-Trigger: newContact` header. Table listens for the event:

```html

<tbody id="contacts-table"
       hx-get="/contacts/table"
       hx-trigger="newContact from:body">
...
</tbody>
```

## Morphing

`innerMorph` and `outerMorph` merge new content into the existing DOM instead of replacing it.

**Preserves:** focus, scroll position, CSS animations, event listeners, playing video, form input values.

**ID matching** is highest priority -- elements with matching IDs are updated in place.

**Warning:** morphing preserves user input values. It cannot be used to reset forms -- use `innerHTML`/`outerHTML` for
that.

**Excluding elements from morphing** — add attributes to your server templates:

```html
<!-- freeze entire element: attrs + children unchanged -->
<custom-widget hx-morph-skip>...</custom-widget>

<!-- freeze only children: attrs still update -->
<lit-component hx-morph-skip-children>...</lit-component>
```

Or set CSS selectors globally in config:

```javascript
htmx.config.morphSkip         = 'custom-widget, .frozen';
htmx.config.morphSkipChildren = 'lit-component, .sortable';
```

## Other Attributes

| Attribute        | Description                                                               |
|------------------|---------------------------------------------------------------------------|
| `hx-select`      | CSS selector to pick part of the response                                 |
| `hx-select-oob`  | Pick out elements by ID for OOB swap                                      |
| `hx-include`     | Include additional elements' values in request                            |
| `hx-vals`        | Add values to request. Supports `js:` prefix for dynamic values           |
| `hx-headers`     | Add custom headers to request                                             |
| `hx-indicator`   | Element to show during request (gets `htmx-request` class)                |
| `hx-confirm`     | Show confirmation dialog. Supports `js:` prefix for async confirmation    |
| `hx-sync`        | Synchronize requests between elements                                     |
| `hx-boost`       | Progressive enhancement for links and forms                               |
| `hx-config`      | Per-element Fetch config (`timeout`, `credentials`, `cache`, etc.). Cannot override `mode` |
| `hx-preserve`    | Keep element unchanged across swaps                                       |
| `hx-ignore`      | Disable htmx processing for element and children                          |
| `hx-disable`     | Disable specified elements during requests                                |
| `hx-preload`     | Preload content on trigger events                                         |
| `hx-optimistic`  | Show optimistic content during request                                    |
| `hx-push-url`    | Push URL to browser history                                               |
| `hx-replace-url` | Replace URL in browser history                                            |
| `hx-encoding`    | Change encoding (e.g. `multipart/form-data` for file uploads)             |
| `hx-validate`    | Validate form elements before request                                     |

## Parameters

- Non-GET/DELETE requests automatically include enclosing form values
- GET and DELETE do NOT include enclosing form data. Use `hx-include="closest form"` if needed
- Use `hx-vals='{"key": "value"}'` for static values
- Use `hx-vals='js:{"key": computeValue()}'` for dynamic values

## JavaScript API

```js
htmx.ajax("GET", "/data", {target: "#result"})  // Programmatic request, returns Promise
htmx.on("htmx:after:swap", (evt) => { ...
})    // Event listener
htmx.onLoad((elt) => { ...
})                    // Process new content
htmx.process(element)                            // Initialize htmx on dynamic content
htmx.find("closest .container")                  // Extended CSS selector query
htmx.findAll(".items")                           // Find all matching
htmx.trigger(elt, "myEvent", {detail: ...})      // Fire custom event
htmx.swap(ctx)                                   // Manual swap
htmx.timeout(1000)                               // Promise that resolves after delay
htmx.live.take(elt, "active", ".tab")            // Take class — provided by hx-live
htmx.live.forEvent(elt, "click", 5000)           // Race events/timeouts — provided by hx-live
htmx.live.nextFrame()                            // requestAnimationFrame promise — provided by hx-live
```

## Common Patterns

### Active Search

```html
<input type="text" name="q"
       hx-get="/search"
       hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
       hx-target="#search-results"
       hx-indicator="#spinner">
<span id="spinner" class="htmx-indicator">Searching...</span>
<div id="search-results"></div>
```

### Lazy Loading

```html

<div hx-get="/lazy-content" hx-trigger="load" hx-swap="outerHTML">
    Loading...
</div>
```

### Infinite Scroll

```html

<tr hx-get="/page/3" hx-trigger="revealed" hx-swap="afterend">
    <!-- last row of current page -->
</tr>
```

### Click to Load More

```html

<button hx-get="/page/2" hx-target="#results" hx-swap="beforeend">
    Load More
</button>
```

### Edit in Place

```html

<div hx-get="/contact/1/edit" hx-trigger="click" hx-swap="outerHTML">
    <p>Click to edit</p>
</div>
```

Server returns an edit form. Form submits via hx-post and returns the display view.

### Tabs

```html

<div role="tablist" hx-target:inherited="#tab-content">
    <button role="tab" hx-get="/tab/1" class="active">Tab 1</button>
    <button role="tab" hx-get="/tab/2">Tab 2</button>
</div>
<div id="tab-content">...</div>
```

### Form Validation

```html

<form hx-post="/register"
      hx-target="#result"
      hx-status:422="target:#errors">
    <input name="email" type="email">
    <div id="errors"></div>
    <div id="result"></div>
    <button type="submit">Register</button>
</form>
```

Server returns 422 with error HTML, target becomes the element with the errors id, or 200 with success HTML target is
the element with the id `result`.

### Loading Indicators

```html

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

The `htmx-indicator` class hides the element by default (opacity: 0). When a request is in flight, `htmx-request` class
is added, making indicators visible.

To avoid flashing the spinner on fast requests, add a `transition-delay` (the second time value) to the indicator's CSS:

```css
.htmx-request .htmx-indicator { transition: opacity 200ms ease-in 200ms; }
```

If the request finishes before the delay elapses, the spinner never appears

### Disabling Elements During Request

```html

<form hx-post="/save" hx-disable="find button, find input">
    <input name="data">
    <button type="submit">Save</button>
</form>
```

## Extensions

Extensions are loaded by including the script file. They apply page-wide automatically:

```html
<script src="/path/to/hx-preload.js"></script>
```

To restrict which extensions can load, use the `extensions` config as a whitelist:

```html
<meta name="htmx-config" content='{"extensions": "preload"}'>
```

## htmx 2 vs htmx 4: Practical Differences

If you're unsure which version a project uses, check for `fetch()` usage in htmx source, the `:inherited`
modifier on attributes, or colon-separated event names like `htmx:after:swap`. These are all htmx 4 indicators.

### Attributes

| htmx 2                               | htmx 4                                                        | Notes                                             |
|--------------------------------------|---------------------------------------------------------------|---------------------------------------------------|
| `hx-disabled-elt`                    | `hx-disable`                                                  | Renamed                                           |
| `hx-disable` (stops htmx processing) | `hx-ignore`                                                   | Different purpose in each version                 |
| `hx-ext="my-ext"`                    | Just include the script file                                  | No attribute needed; config whitelist is optional  |
| `hx-request='{"timeout":5000}'`      | `hx-config='{"timeout":5000}'`                                | Renamed                                           |
| `hx-prompt="Enter value"`            | [`hx-prompt` extension](/extensions/hx-prompt) (same syntax), or [`hx-on::config:request` one-liner](/extensions/hx-prompt#without-the-extension) | Restored via extension                            |
| `hx-disinherit="*"`                  | Not needed                                                    | Inheritance is explicit by default in htmx 4      |
| `hx-vars`                            | `hx-vals` with `js:` prefix                                   | hx-vars removed                                   |
| Attributes inherit implicitly        | Must use `:inherited` modifier                                | `hx-target:inherited="#out"`                      |
| `data-hx-get` works automatically    | Requires `config.prefix = "data-hx-"`                         | Only `hx-*` by default in htmx 4                  |

htmx 4 adds: `hx-action`, `hx-method`, `hx-config`, `hx-status:XXX`, `hx-partial`, `:inherited` and `:append` modifiers.

### Events

htmx 2 uses camelCase: `htmx:afterSwap`, `htmx:beforeRequest`, `htmx:configRequest`.

htmx 4 uses colons: `htmx:after:swap`, `htmx:before:request`, `htmx:config:request`.

Most error events (`htmx:sendError`, `htmx:swapError`, `htmx:targetError`, `htmx:timeout`) are consolidated into
`htmx:error` in htmx 4. HTTP error responses fire `htmx:response:error` (replacing `htmx:responseError`).

### Configuration

| htmx 2                              | htmx 4                               | Notes                            |
|-------------------------------------|--------------------------------------|----------------------------------|
| `htmx.config.defaultSwapStyle`      | `htmx.config.defaultSwap`            | Renamed                          |
| `htmx.config.timeout = 0`           | `htmx.config.defaultTimeout = 60000` | Renamed + default changed to 60s |
| `htmx.config.globalViewTransitions` | `htmx.config.transitions`            | Renamed                          |
| `htmx.config.historyEnabled`        | `htmx.config.history`                | Renamed                          |
| `htmx.config.selfRequestsOnly`      | `htmx.config.mode = 'same-origin'`   | Different mechanism              |
| `responseHandling` array            | `htmx.config.noSwap` + `hx-status`   | Simpler model                    |
| 4xx/5xx don't swap by default       | All status codes swap except 204/304 | Major behavior change            |
| History stored in localStorage      | History does full page refresh       | No more localStorage snapshots   |

### JavaScript API

| htmx 2                                        | htmx 4                      | Notes                            |
|-----------------------------------------------|-----------------------------|----------------------------------|
| `htmx.defineExtension()`                      | `htmx.registerExtension()`  | Renamed                          |
| `htmx.addClass()`, `htmx.removeClass()`, etc. | Native DOM methods          | Removed; use `element.classList` |
| `htmx.off()`                                  | `removeEventListener()`     | Removed; use native              |
| `htmx.remove()`                               | `element.remove()`          | Removed; use native              |
| `htmx.swap(target, content, spec)`            | `htmx.swap(ctx)`            | Signature changed                |

htmx 4 adds: `htmx.timeout()`. Logging now goes directly to `console.error` / `console.warn` / `console.log` (gated by `config.logAll` for events). `htmx.takeClass()` is **removed**; use `htmx.live.take()` (provided by the `hx-live` extension) or the unprefixed `take` helper inside expression scope. The `hx-live` extension also exposes `htmx.live.forEvent()`, `htmx.live.nextFrame()`, `htmx.live.q()`, `htmx.live.debounce()`, `htmx.live.refresh()`.

### Swap Styles

htmx 4 adds `innerMorph`, `outerMorph`, `textContent`, and shorthand names (`before`, `after`, `prepend`, `append`).

### HTTP Headers

| htmx 2                               | htmx 4      | Notes                                       |
|--------------------------------------|-------------|---------------------------------------------|
| `HX-Trigger` (request header)        | `HX-Source` | Renamed; format changed from ID to `tag#id` |
| `HX-Trigger-Name`                    | Removed     | Use `HX-Source`                             |
| `HX-Trigger-After-Swap` (response)   | Removed     | Use `HX-Trigger`                            |
| `HX-Trigger-After-Settle` (response) | Removed     | Use `HX-Trigger`                            |

htmx 4 adds: `HX-Request-Type` (`"full"` or `"partial"`).

### Extensions

htmx 2: `hx-ext="my-extension"` attribute on elements, `htmx.defineExtension("name", {onEvent: ...})`.

htmx 4: Just include the script. `htmx.registerExtension("name", {htmx_before_request: ...})`. Config whitelist optional.
Hook names use underscores (`htmx_before_swap`) instead of a single `onEvent` callback.

## Instructions for Claude

When generating htmx code:

1. The general vibe with htmx is simplicity: a request returns HTML that is inserted into the DOM
1. **Use `:inherited` modifier** for any attribute on a parent element intended for children
1. **Server endpoints must return HTML fragments**, not JSON
1. **Add loading indicators** for requests that may take time (`hx-indicator` + element with `htmx-indicator` class)
1. **Use `hx-status:422`** for validation error handling -- server returns 422 with error HTML
1. **Use morph swaps** when preserving form/input state matters. Use `innerHTML`/`outerHTML` for clean replacement
1. **Prefer `<hx-partial>` tags** over `hx-swap-oob` for multi-region updates (more explicit)
1. **GET and DELETE don't include form data** -- use `hx-include="closest form"` if needed
1. When showing patterns, include both the HTML and describe what the server endpoint should return
1. There are many useful extensions, for examples sse.js (Server Sent Events) for more dynamic situation and
   hx-preload.js for speeding up navigational requests. Suggest them if they make sense.