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:

ElementDefault trigger
input, textarea, selectchange
formsubmit
everything elseclick

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:

ModifierMeaning
oncefire only the first time
changedfire 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 loads
  • revealed - fires when the element first scrolls into the viewport (lazy loading); inside a CSS overflow: scroll container use intersect once instead
  • intersect - fires on viewport intersection, with root: and threshold: options
  • every <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, so from:button will 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: changed is a modifier, change is an event name. hx-trigger="change changed" is legal but rarely what you mean; on inputs you usually want hx-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:

SelectorResolves to
thisthe 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:

StyleEffect
innerHTML (default)replace inner content
outerHTMLreplace the whole element
afterbegin / beforeendinsert inside, at the top / bottom
beforebegin / afterendinsert outside, before / after the element
deleteremove the element regardless of response
nonedo not swap (still fires events)

Modifiers (space-separated after the style):

ModifierDefaultMeaning
swap:<time>0msdelay before the swap happens
settle:<time>20mswindow where attributes from the old element are copied to the new one, so CSS transitions run
transition:<bool>falseopt 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>falseskip 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/value is always included.
  • If the element is inside a form (or is one), a non-GET request includes all the form’s inputs.
  • hx-include adds other elements’ inputs; hx-vals adds static JSON; hx-params filters 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.