Implements HTMX interactions, configures swap behaviors, debugs hx-* requests, and builds hypermedia-driven UI components. Use when tasks involve hx-* attributes, HTMX AJAX requests, swap strategies, server-sent events, WebSockets, or hypermedia-driven UIs.
95
95%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
All hx-* attributes, their accepted values, modifiers, and behavior.
Note: All
hx-*attributes can also be written asdata-hx-*for HTML validation compliance (e.g.,data-hx-get,data-hx-trigger).
These attributes issue HTTP requests when the element is triggered.
<!-- GET request -->
<button hx-get="/api/items">Load Items</button>
<!-- POST request -->
<form hx-post="/api/items">
<input name="title" />
<button type="submit">Create</button>
</form>
<!-- PUT request -->
<button hx-put="/api/items/1">Update</button>
<!-- PATCH request -->
<button hx-patch="/api/items/1">Patch</button>
<!-- DELETE request -->
<button hx-delete="/api/items/1">Delete</button>| Attribute | HTTP Method | Notes |
|---|---|---|
hx-get | GET | Parameters appended to URL as query string |
hx-post | POST | Parameters sent in request body |
hx-put | PUT | Parameters sent in request body |
hx-patch | PATCH | Parameters sent in request body |
hx-delete | DELETE | Parameters appended to URL by default |
Value: A URL string (relative or absolute). Relative URLs resolve against the current page.
hx-trigger specifies which event(s) cause the request to fire.
Important:
hx-triggeris NOT inherited. It must be set on each element individually.
hx-trigger is omitted)| Element Type | Default Event |
|---|---|
<input>, <textarea>, <select> | change |
<form> | submit |
| Everything else | click |
<!-- Single event -->
<div hx-get="/data" hx-trigger="click">Click me</div>
<!-- Multiple events (comma-separated) -->
<div hx-get="/data" hx-trigger="click, keyup">Either works</div>
<!-- Event with modifiers -->
<input hx-get="/search" hx-trigger="keyup changed delay:500ms" />
<!-- Event with filter -->
<div hx-get="/data" hx-trigger="click[ctrlKey]">Ctrl+Click only</div>
<!-- Event from another element -->
<div hx-get="/data" hx-trigger="click from:body">Body click</div>
<!-- Polling -->
<div hx-get="/status" hx-trigger="every 2s">Status: loading...</div>
<!-- Intersection observer -->
<img hx-get="/image" hx-trigger="intersect threshold:0.5" />| Modifier | Effect |
|---|---|
once | Trigger only fires once |
changed | Only fire if the element's value has changed |
delay:<time> | Wait before issuing request; resets if event fires again (debounce) |
throttle:<time> | Wait before issuing request; discard events during wait (throttle) |
from:<CSS selector> | Listen for event on a different element. Also accepts document, window, closest <selector>, find <selector>, next, next <selector>, previous, previous <selector>. Note: Selectors with whitespace require parentheses: from:(form input) |
target:<CSS selector> | Only fire if the event target matches the selector |
consume | Prevent event from propagating to parent elements |
queue:<strategy> | Queue behavior when event fires during an active request. Values: first, last (default), all, none |
JavaScript expressions inside square brackets. The event object is available as event. Standard event properties work: ctrlKey, shiftKey, altKey, metaKey, key, etc.
<!-- Only on Ctrl+Click -->
<div hx-get="/data" hx-trigger="click[ctrlKey]">Ctrl+Click</div>
<!-- Only on Enter key -->
<input hx-get="/search" hx-trigger="keyup[key=='Enter']" />
<!-- Custom condition -->
<div hx-get="/data" hx-trigger="click[checkCondition()]">Conditional</div>| Trigger | When It Fires |
|---|---|
load | When the element is loaded into the DOM |
revealed | When the element first scrolls into the viewport |
intersect | When the element intersects the viewport. Options: root:<selector>, threshold:<float> |
every <time> | Polls at the given interval (e.g., every 2s, every 500ms) |
Stopping polling: The server responds with HTTP status 286 to signal the client to stop polling.
Combine load trigger with delay for server-driven polling:
<div hx-get="/status" hx-trigger="load delay:1s" hx-swap="outerHTML">
Checking status...
</div>The server returns a new element with the same trigger to continue, or without it to stop.
Specifies which element receives the response content.
<!-- CSS selector -->
<button hx-get="/data" hx-target="#results">Load</button>
<!-- this (current element) -->
<button hx-get="/data" hx-target="this">Replace me</button>
<!-- Extended selectors -->
<button hx-get="/data" hx-target="closest div">Nearest div ancestor</button>
<button hx-get="/data" hx-target="find .content">First .content descendant</button>
<button hx-get="/data" hx-target="next .sibling">Next .sibling in DOM</button>
<button hx-get="/data" hx-target="previous .sibling">Previous .sibling in DOM</button>| Extended Selector | Meaning |
|---|---|
this | The element itself |
closest <selector> | Nearest ancestor matching selector |
find <selector> | First descendant matching selector |
next | Next sibling element |
next <selector> | Next element in DOM matching selector |
previous | Previous sibling element |
previous <selector> | Previous element in DOM matching selector |
Default: Without hx-target, the element that made the request is the target.
Controls how the response content is inserted into the target. See references/swapping.md for full details.
Values: innerHTML (default), outerHTML, textContent, beforebegin, afterbegin, beforeend, afterend, delete, none
Pick a subset of the response to swap in using a CSS selector.
<!-- Only swap in the #results element from the response -->
<button hx-get="/page" hx-select="#results" hx-target="#results">Load</button>Pick specific elements from the response for out-of-band swaps.
<!-- Swap #results into target, and also OOB swap #notification -->
<button hx-get="/page" hx-select="#results" hx-select-oob="#notification">Load</button>Add extra values to the request as a JSON object.
<!-- Static JSON values -->
<button hx-post="/action" hx-vals='{"key": "value", "count": 42}'>Submit</button>
<!-- Dynamic values with js: prefix -->
<button hx-post="/action" hx-vals='js:{timestamp: Date.now()}'>Submit</button>Comma-separated name-expression pairs evaluated as JavaScript.
Include values from additional elements in the request.
<!-- Include specific input -->
<button hx-post="/action" hx-include="#extra-field">Submit</button>
<!-- Include all inputs in a container -->
<button hx-post="/action" hx-include="#sidebar">Submit</button>
<!-- Extended selectors work -->
<button hx-post="/action" hx-include="closest form">Submit</button>Filter which parameters are submitted.
<!-- Only include listed params -->
<form hx-post="/action" hx-params="name, email">...</form>
<!-- Exclude listed params -->
<form hx-post="/action" hx-params="not password">...</form>
<!-- Include all (default) -->
<form hx-post="/action" hx-params="*">...</form>
<!-- Include none -->
<form hx-post="/action" hx-params="none">...</form>Set the encoding type for the request.
<!-- Required for file uploads -->
<form hx-post="/upload" hx-encoding="multipart/form-data">
<input type="file" name="document" />
<button type="submit">Upload</button>
</form>Add custom headers to the request as a JSON object.
<!-- Static headers -->
<div hx-get="/api" hx-headers='{"X-Custom": "value"}'>Load</div>
<!-- Dynamic headers with js: prefix -->
<div hx-get="/api" hx-headers='js:{"Authorization": "Bearer " + getToken()}'>Load</div>Push a URL into the browser's history stack after a successful request.
<!-- Push the request URL -->
<a hx-get="/page" hx-push-url="true">Navigate</a>
<!-- Push a custom URL -->
<a hx-get="/api/page" hx-push-url="/page">Navigate</a>
<!-- Disable (override inherited) -->
<a hx-get="/page" hx-push-url="false">No history</a>Replace the current URL in the browser's location bar (no new history entry).
<a hx-get="/page" hx-replace-url="true">Update URL</a>
<a hx-get="/api/page" hx-replace-url="/page">Custom URL</a>Prevent the current page from being saved in the history cache.
<!-- On the page's body or a top-level element -->
<body hx-history="false">
<!-- Sensitive content not cached -->
</body>Specify which element should be snapshot and restored during history navigation (default: <body>). This element must always be visible in the application for proper history restoration. Not inherited.
<div id="content" hx-history-elt>
<!-- Only this element is cached/restored -->
</div>Specify the element that gets the htmx-request class during requests, making indicators visible.
<button hx-get="/data" hx-indicator="#spinner">
Load
</button>
<span id="spinner" class="htmx-indicator">Loading...</span>The htmx-indicator class sets opacity: 0 by default. When the parent (or hx-indicator target) receives htmx-request, the indicator becomes visible (opacity: 1).
Multiple indicators:
<button hx-get="/data" hx-indicator=".my-indicator">Load</button>Add the disabled attribute to elements while a request is in flight.
<!-- Disable this element -->
<button hx-post="/action" hx-disabled-elt="this">Submit</button>
<!-- Disable another element -->
<button hx-post="/action" hx-disabled-elt="#other-btn">Submit</button>
<!-- Disable multiple elements -->
<button hx-post="/action" hx-disabled-elt="closest form, #other-btn">Submit</button>Show a browser confirm() dialog before issuing the request.
<button hx-delete="/item/1" hx-confirm="Are you sure?">Delete</button>Use hx-confirm="unset" on a child to disable an inherited confirm.
Show a browser prompt() dialog. The user's response is sent in the HX-Prompt request header.
<button hx-post="/rename" hx-prompt="Enter new name:">Rename</button>Most hx-* attributes inherit from parent to child elements. Not inherited: hx-trigger, hx-on*, hx-swap-oob, hx-preserve, hx-history-elt, hx-validate.
Disable inheritance of specific attributes for child elements.
<!-- Disable all inheritance -->
<div hx-confirm="Sure?" hx-disinherit="*">
<button hx-delete="/item/1">No confirm dialog</button>
</div>
<!-- Disable specific attribute inheritance -->
<div hx-target="#results" hx-disinherit="hx-target">
<button hx-get="/data">Uses own default target</button>
</div>Explicitly enable inheritance for specific attributes (useful when htmx.config.disableInheritance = true).
<div hx-target="#results" hx-inherit="hx-target">
<button hx-get="/data">Inherits hx-target</button>
</div>Use "unset" as the value to clear an inherited attribute:
<div hx-confirm="Sure?">
<button hx-get="/safe" hx-confirm="unset">No confirm</button>
</div>Completely prevents HTMX processing on the element and all its children. Cannot be overridden by injected content.
<div hx-disable>
<!-- No HTMX processing here, even if attributes are present -->
<button hx-get="/data">This will NOT make a request</button>
</div>Convert standard anchors and forms into AJAX requests targeting the <body>.
<!-- Boost all links and forms in this container -->
<div hx-boost="true">
<a href="/page">AJAX navigation</a>
<form action="/submit" method="post">
<button type="submit">AJAX submit</button>
</form>
</div>Boosted anchors push the URL into browser history. Boosted forms do NOT push URL — add hx-push-url="true" explicitly if needed. Boosted pages must serve full HTML pages. They degrade gracefully when JS is disabled. Only same-domain links are boosted; local anchor links are ignored.
Enable HTMX extensions on an element and its children.
<!-- Single extension -->
<div hx-ext="preload">...</div>
<!-- Multiple extensions -->
<div hx-ext="preload, response-targets">...</div>
<!-- Disable an inherited extension -->
<div hx-ext="ignore:preload">...</div>Keep an element unchanged across swaps. The element must have an id. Useful for video players, iframes, or complex widgets.
<video id="player" hx-preserve>
<source src="video.mp4" />
</video>Control how requests from multiple elements are synchronized.
<!-- Abort this request if the form submits -->
<input hx-get="/search" hx-trigger="keyup" hx-sync="closest form:abort" />
<!-- Drop new requests while one is in flight -->
<button hx-post="/action" hx-sync="this:drop">Submit</button>
<!-- Queue requests, keep last -->
<input hx-get="/search" hx-sync="this:queue last" />| Strategy | Behavior |
|---|---|
drop | Drop new request if one is in flight |
abort | Abort the current request and issue new one |
replace | Abort current request, issue new one (alias for abort) |
queue first | Queue first request, drop subsequent |
queue last | Queue most recent request, drop earlier queued |
queue all | Queue all requests |
Force elements to validate themselves before a request is issued.
<input hx-get="/check" hx-validate="true" required pattern="[a-z]+" />Configure various aspects of the request.
<!-- Set timeout -->
<button hx-get="/slow" hx-request='"timeout": 5000'>Load</button>
<!-- Disable credentials -->
<button hx-get="/api" hx-request='"credentials": false'>Load</button>
<!-- Disable no-cache header -->
<button hx-get="/api" hx-request='"noHeaders": true'>Load</button>Respond to any event directly with inline JavaScript. Uses hx-on:<event-name> or hx-on-<event-name> syntax (colon or dash separator).
<!-- Standard DOM events -->
<button hx-on:click="alert('clicked!')">Click</button>
<!-- HTMX events (use kebab-case for camelCase events) -->
<form hx-post="/submit" hx-on:htmx:before-request="showSpinner()">
<button type="submit">Submit</button>
</form>
<!-- Modify request parameters -->
<div hx-get="/data" hx-on:htmx:config-request="event.detail.parameters.extra = 'value'">
Load
</div>Note: HTML attributes are case-insensitive. Use kebab-case for camelCase event names (e.g., htmx:config-request instead of htmx:configRequest).