Core Attributes
Five request attributes do the issuing. Everything else configures when, where, and what:
<button hx-get="/stories">Get</button>
<form hx-post="/stories">...</form>
<form hx-put="/stories/42">...</form>
<form hx-patch="/stories/42">...</form>
<button hx-delete="/stories/42">Delete</button>
Each takes a URL. With no other attributes, the element uses its natural trigger and targets itself.
Natural triggers
Without hx-trigger, htmx picks a sensible default per element:
| Element | Default trigger |
|---|---|
input, textarea, select | change |
form | submit |
| everything else | click |
Override it with hx-trigger:
<div hx-get="/weather" hx-trigger="every 30s">...</div>
hx-trigger
hx-trigger takes a comma-separated list of events, each with optional modifiers and filters:
<input name="q"
hx-get="/search"
hx-trigger="input changed delay:500ms, search"
hx-target="#results">
Two triggers: input (debounced), and the native search event so pressing Enter fires immediately.
Modifiers:
| Modifier | Meaning |
|---|---|
once | fire only the first time |
changed | fire only if the element’s value changed since last time |
delay:<time> | wait before firing; a new event resets the timer (debounce) |
throttle:<time> | fire, then ignore events until the interval elapses |
from:<selector> | listen on another element instead (document, window, closest <s>, next <s>, previous <s>, find <s> all work) |
queue:<mode> | what to do with events that arrive mid-request (first, last, all, none) |
Special events:
load- fires when the element loadsrevealed- fires when the element first scrolls into the viewport (lazy loading); inside a CSSoverflow: scrollcontainer useintersect onceinsteadintersect- fires on viewport intersection, withroot:andthreshold:optionsevery <time>- polling
Polling stops when the server answers with HTTP status code 286 - a deliberate htmx convention, verified at src/htmx.js:4948:
<div hx-get="/progress" hx-trigger="every 1s">...</div>
Filters: square brackets hold a JavaScript expression evaluated against the event; the request fires only when it returns truthy:
<div hx-get="/help" hx-trigger="keyup[key=='?'] from:body">...</div>
Gotcha: a
from:selector is evaluated once, when the element processes the trigger. It is not re-evaluated when the DOM changes, sofrom:buttonwill not pick up buttons added later. Catch dynamic elements with a filter on a stable ancestor instead:hx-trigger="click[event.target.matches('button')] from:body".
Gotcha:
changedis a modifier,changeis an event name.hx-trigger="change changed"is legal but rarely what you mean; on inputs you usually wanthx-trigger="input changed delay:500ms".
hx-target
By default the response swaps into the triggering element. hx-target redirects it, with extended selectors beyond CSS:
| Selector | Resolves to |
|---|---|
this | the element itself (also usable as hx-target="this" on a parent) |
closest <selector> | nearest ancestor (or self) matching the selector |
find <selector> | first descendant matching |
next <selector> | next element in the DOM matching |
previous <selector> | previous element matching |
Relative targets are the habit that keeps htmx pages maintainable - hx-target="closest tr" beats sprinkling ids through a table:
<tr>
<td>Mombasa trip</td>
<td><button hx-delete="/trips/7" hx-target="closest tr" hx-swap="outerHTML">Delete</button></td>
</tr>
hx-swap
How the response replaces the target’s content:
| Style | Effect |
|---|---|
innerHTML (default) | replace inner content |
outerHTML | replace the whole element |
afterbegin / beforeend | insert inside, at the top / bottom |
beforebegin / afterend | insert outside, before / after the element |
delete | remove the element regardless of response |
none | do not swap (still fires events) |
Modifiers (space-separated after the style):
| Modifier | Default | Meaning |
|---|---|---|
swap:<time> | 0ms | delay before the swap happens |
settle:<time> | 20ms | window where attributes from the old element are copied to the new one, so CSS transitions run |
transition:<bool> | false | opt into the View Transitions API for this swap (enable globally with htmx.config.globalViewTransitions) |
scroll:<top|bottom> | - | scroll the target after swap |
show:<top|bottom> | - | scroll the browser window |
ignoreTitle:<bool> | false | skip updating <title> from the response |
Defaults are grounded in source: defaultSwapDelay: 0 (src/htmx.js:106), defaultSettleDelay: 20 (src/htmx.js:112).
Indicators
Give any element the htmx-indicator class and htmx toggles its visibility during requests. The htmx-request class lands on the issuing element while a request is in flight:
<button hx-get="/reports" hx-indicator="#spinner">
Run report
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.svg" alt="Loading">
hx-indicator accepts a selector so the indicator can live anywhere. htmx-indicator works by opacity, so restyle it freely with CSS.
Use hx-disabled-elt="this" to disable buttons while the request runs - simpler than an indicator when the feedback is just “don’t double-submit”.
Parameters
What gets sent:
- The triggering element’s own
name/valueis always included. - If the element is inside a form (or is one), a non-GET request includes all the form’s inputs.
hx-includeadds other elements’ inputs;hx-valsadds static JSON;hx-paramsfilters which parameters are sent.hx-encoding="multipart/form-data"switches the request to multipart for file uploads.
<form hx-post="/stories" hx-encoding="multipart/form-data">
<input type="text" name="title">
<input type="file" name="cover">
<button>Publish</button>
</form>
hx-confirm="Are you sure?" shows a native confirm dialog before the request. For custom dialogs, handle the htmx:confirm event and call evt.detail.issueRequest().
Inheritance
Most attributes inherit to children, so you can hoist shared config up the tree:
<tbody hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML">
<tr>
<td><button hx-delete="/contacts/1">Delete</button></td>
</tr>
<tr>
<td><button hx-delete="/contacts/2">Delete</button></td>
</tr>
</tbody>
Opt out per element with hx-confirm="unset" or per attribute with hx-disinherit.
Gotcha: inheritance is why an htmx attribute on a wrapper sometimes changes behavior of elements far away. When a trigger mysteriously double-fires, check the ancestors before the element itself.
Next: Server Integration covers what your server sees and returns.