---
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 |
| `hx-query` | QUERY 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
Hover me
```
**Modifiers:**
| Modifier | Description |
|---------------------|--------------------------------------------------------------------------------------|
| `once` | fire only once |
| `changed` | only fire if the value of the element changed |
| `delay:` | debounce, e.g. `delay:500ms`. A new event resets the countdown |
| `throttle:` | throttle. The first event fires at once, later events wait for the cooldown |
| `from:` | listen on a different element. Accepts `document`, `window`, `closest`, `find`, `next`, `previous` |
| `target:` | only fire if `event.target` matches the selector |
| `prevent` | call `event.preventDefault()` |
| `stop` | call `event.stopPropagation()`. `consume` is a synonym |
| `halt` | shorthand for `prevent stop` |
| `capture` | listen in the capture phase instead of the bubble phase |
| `passive` | tell the browser the handler will not call `preventDefault()` |
A selector with whitespace needs parentheses: `from:(form input)`.
**Filters** (JavaScript expressions in brackets):
```html
Ctrl+Click me
```
**Special events:**
- `load` -- fires when element is loaded
- `revealed` -- fires when element scrolls into viewport
- `intersect` -- fires on intersection (options: `root:`, `threshold:`)
**Polling:**
```html
Poll
```
**Multiple triggers** (comma-separated):
```html
```
**Triggering from HX-Trigger header** -- use `from:body`:
```html
...
```
## 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 ` -- nearest ancestor matching selector
- `find ` -- 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
Load
```
## 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 |
| `outerSync` | Morph the target's attributes, then replace its children. Target stays in DOM |
| `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
```
| Modifier | Description |
|---------------------|----------------------------------------------|
| `swap:
` | Delay before swap |
| `settle:` | 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 |
| `scrollTarget:` | Scroll this element instead of the target |
| `showTarget:` | Scroll this element into view instead of the target |
| `strip:true` | Remove outer wrapper element before swapping |
| `focusScroll:true` | Scroll to focused element |
| `swapEmpty:true` | Run the main swap even when nothing remains after OOB and partial content is removed |
| `target:` | 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
A
B
A
B
```
The `:append` modifier appends to inherited values:
```html
Save
```
To revert to implicit inheritance globally: set `htmx.config.implicitInheritance = true`.
## Configuration
Set via meta tag or JavaScript:
```html
```
Key config values:
| Config | Default | Description |
|--------------------------|------------------------------|----------------------------------------------------------------|
| `defaultSwap` | `innerHTML` | Default swap strategy |
| `defaultTimeout` | `60000` | Request timeout (ms) |
| `defaultSettleDelay` | `1` | Delay in ms between swap and settle |
| `defaultFocusScroll` | `false` | Scroll focused elements into view after a swap |
| `noSwap` | `[204, 304]` | Status codes that skip swapping |
| `allowEmptySwapAfterOOB` | `false` | Run the main swap when the response holds only OOB or partial content |
| `implicitInheritance` | `false` | Auto-inherit attributes from parents |
| `transitions` | `false` | Enable View Transitions globally |
| `logAll` | `false` | Log every event 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 allows all |
| `prefix` | `"data-hx-"` | Second attribute prefix, checked in addition to `hx-` |
| `metaCharacter` | unset, acts as `:` | Character that introduces an attribute modifier |
| `indicatorClass` | `htmx-indicator` | Class on elements that show during a request |
| `requestClass` | `htmx-request` | Class added while a request is in flight |
| `includeIndicatorCSS` | `true` | Inject the default indicator stylesheet |
| `inlineScriptNonce` | unset | Nonce added to scripts htmx inserts |
| `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 |
`prefix` is additive, not a replacement. `hx-get` and `data-hx-get` both work out of the box.
Config values use HCON, htmx's configuration object notation. HCON accepts JSON, but also a shorter form:
```html
```
## Events
htmx 4 naming convention: `htmx:phase:action`
**Element lifecycle:**
- `htmx:before:process` / `htmx:after:process` -- htmx scans a subtree
- `htmx:before:init` / `htmx:after:init` -- element initialization
- `htmx:before:cleanup` / `htmx:after:cleanup` -- element removal
- `htmx:before:on:init` -- before an `hx-on` handler is installed
**Request:**
- `htmx:confirm` -- after trigger, before request. Detail holds `issueRequest` and `dropRequest` for async confirmation
- `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)
- `htmx:response:error` -- the server returned an HTTP error status
**Swap:**
- `htmx:before:swap` / `htmx:after:swap` -- before/after content swap
- `htmx:finally:swap` -- after the swap, on success or error
- `htmx:before:settle` / `htmx:after:settle` -- before/after settle phase
**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`
**Aborting a request:** `htmx:abort` is an event you dispatch, not one htmx fires. Send it at an element to
cancel that element's in-flight requests:
```js
htmx.trigger("#slow-thing", "htmx:abort");
```
A few hooks are delivered to extensions only and never reach the DOM: `htmx:before:morph:node`,
`htmx:before:morph:attr`, `htmx:after:implicitInheritance` and `htmx:process:`.
### 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
Load
```
## 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
```
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
```
Server returns both the table and the form.
### 2. Out-of-Band Swaps
Server response includes extra elements with `hx-swap-oob`:
```html
New row
```
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
New message
5
```
Each `` 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
...
```
## 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
...
...
```
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-pending` | Show pending 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 |
| `hx-action` | Request URL, when the method comes from `hx-method` |
| `hx-method` | HTTP method, paired with `hx-action` |
| `hx-status:XXX` | Change target, swap or history handling for one status code |
| `hx-history-elt` | Element to restore on history navigation, instead of `body` |
| `hx-morph-skip` | Freeze this element during a morph swap |
| `hx-morph-skip-children` | Update attributes but freeze children during a morph swap |
## 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. `hx-vals` takes HCON, which also accepts JSON
- Use `hx-vals='js:{"key": computeValue()}'` for dynamic values
- `hx-headers` and `hx-config` take HCON too
## JavaScript API
```js
htmx.version // Version string, read-only
htmx.ajax("GET", "/data", {target: "#result"}) // Programmatic request, returns Promise
htmx.on("htmx:after:swap", (evt) => {}) // Event listener
htmx.onLoad((elt) => {}) // Callback for new content
htmx.process(element) // Initialize htmx on dynamic content
htmx.initialize() // Set up history and process document.body
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 a delay
htmx.parseInterval("2s") // Parse a time interval to ms
htmx.registerExtension("name", hooks) // Register an extension
```
The `hx-live` extension adds an `htmx.live` namespace: `take()`, `toggle()`, `attr()`, `q()` (alias `$`),
`debounce()`, `refresh()`, `forEvent()` and `nextFrame()`.
## Common Patterns
### Active Search
```html
Searching...
```
### Lazy Loading
```html
Loading...
```
### Infinite Scroll
```html
```
### Click to Load More
```html
Load More
```
### Edit in Place
```html
```
Server returns an edit form. Form submits via hx-post and returns the display view.
### Tabs
```html
Tab 1
Tab 2
...
```
### Form Validation
```html
```
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
Load
```
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
```
## Extensions
Extensions are loaded by including the script file. They apply page-wide automatically:
```html
```
To restrict which extensions can load, use the `extensions` config as a whitelist. The whitelist takes the
registration name, which is not always the file name:
```html
```
Shipped extensions and their registration names:
| File | Registers as | Purpose |
|---------------------------|---------------------|-----------------------------------------------------|
| `hx-multipart.js` | `hx-multipart` | Stream HTML with `multipart/mixed` |
| `hx-sse.js` | `sse` | Stream HTML with `text/event-stream` (SSE) |
| `hx-ws.js` | `ws` | Stream HTML and send data over WebSockets |
| `hx-browser-indicator.js` | `browser-indicator` | Show the browser tab's own spinner |
| `hx-live.js` | `hx-live` | DOM-based reactive scripting |
| `hx-pending.js` | `hx-pending` | Show custom content during requests |
| `hx-prompt.js` | `hx-prompt` | Restores htmx 2's `hx-prompt` |
| `hx-preload.js` | `preload` | Preload on hover or other triggers |
| `hx-history-cache.js` | `history-cache` | Restore back/forward pages from `sessionStorage` |
| `hx-ptag.js` | `ptag` | Skip unchanged polls with `HX-PTag` |
| `hx-download.js` | `download` | Download files with `hx-swap="download"` |
| `hx-head.js` | `hx-head` | Merge `` tags with `hx-head="merge"` |
| `hx-targets.js` | `hx-targets` | Target many elements with `hx-targets` |
| `hx-upsert.js` | `upsert` | Update or insert elements with `hx-swap="upsert"` |
| `htmx-2-compat.js` | `compat` | Restore htmx 2 defaults and event names |
| `hx-alpine-compat.js` | `alpine-compat` | Run htmx alongside Alpine.js without conflicts |
| `hx-csp.js` | `hx-csp` | Make htmx work under a strict Content Security Policy |
`htmax.js` bundles htmx with the most popular extensions in one file.
## 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](https://four.htmx.org/extensions/hx-prompt) (same syntax), or [`hx-on::config:request` one-liner](https://four.htmx.org/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 | `data-hx-get` still works | `config.prefix` defaults to `"data-hx-"` |
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 `` 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 example `hx-sse.js` for Server-Sent Events and `hx-preload.js` for
faster navigation. Suggest them if they make sense.