htmx Development Guide
htmx gives you access to AJAX, CSS Transitions, WebSockets, and Server-Sent Events directly in HTML using attributes. The core idea: any element can issue HTTP requests, and the response is HTML that gets swapped into the DOM.
The Mental Model
In traditional web apps, only <a> and <form> can make HTTP requests, only click and submit trigger them, and only the full page gets replaced. htmx removes these constraints:
- Any element can issue a request (
<button>,<div>,<span>,<tr>, etc.) - Any event can trigger it (click, input, revealed, load, custom events, polling)
- Any HTTP verb works (GET, POST, PUT, PATCH, DELETE)
- Any part of the page can be the swap target
The server returns HTML fragments, not JSON. This is the fundamental architectural difference from SPA frameworks.
Core Attributes
HTTP Verbs
| Attribute | Description |
|---|---|
hx-get |
Issues a GET request |
hx-post |
Issues a POST request |
hx-put |
Issues a PUT request |
hx-patch |
Issues a PATCH request |
hx-delete |
Issues a DELETE request |
None of the HTTP verb attributes are inherited. An empty value (e.g., hx-get="") requests the current URL.
Example — a button that loads content:
<button hx-get="/api/data" hx-target="#result" hx-swap="innerHTML">
Load Data
</button>
<div id="result"></div>
hx-trigger — What Causes the Request
Controls which event fires the request. Default triggers: click for most elements, change for inputs/selects/textareas, submit for forms.
Syntax: hx-trigger="<event>[<filter>] <modifiers>"
Modifiers:
once— trigger only oncechanged— only if the element's value changeddelay:<time>— debounce (e.g.,delay:500ms)throttle:<time>— throttle (e.g.,throttle:1s)from:<selector>— listen on a different element (supportsdocument,window,closest <sel>,find <sel>,next,previous)target:<selector>— only if the event target matchesconsume— stop event propagationqueue:<first|last|all|none>— queuing strategy
Event filters use JavaScript expressions in brackets:
<input hx-get="/search" hx-trigger="keyup[key=='Enter']"/>
<button hx-get="/action" hx-trigger="click[ctrlKey]">Ctrl+Click</button>
Special triggers:
load— fires when element loadsrevealed— fires when element scrolls into viewportintersect— uses IntersectionObserver (withroot:andthreshold:options)every <time>— polling (e.g.,every 2s)
Multiple triggers are comma-separated:
<input hx-post="/search"
hx-trigger="input changed delay:500ms, keyup[key=='Enter'], load"
hx-target="#results">
Not inherited.
hx-target — Where the Response Goes
CSS selector for the element that receives the response HTML. Defaults to the element making the request.
Extended selectors:
this— the element itselfclosest <sel>— nearest ancestor matching selectorfind <sel>— first descendant matching selectornext/next <sel>— next sibling (optionally matching selector)previous/previous <sel>— previous sibling
<button hx-get="/info" hx-target="closest .card">Load</button>
<button hx-delete="/item/1" hx-target="closest tr">Delete</button>
Inherited.
hx-swap — How Content is Replaced
Controls the swap strategy. Default: innerHTML.
| Value | Description |
|---|---|
innerHTML |
Replace inner content of target |
outerHTML |
Replace the entire target element |
textContent |
Replace text content (no HTML parsing) |
beforebegin |
Insert before target |
afterbegin |
Insert at start of target |
beforeend |
Insert at end of target |
afterend |
Insert after target |
delete |
Delete the target element |
none |
Do nothing with the response |
Swap modifiers (appended with space):
swap:<time>— delay before swap (e.g.,swap:1sfor fade-out animations)settle:<time>— delay before settling (e.g.,settle:1sfor fade-in)transition:true— use View Transitions APIignoreTitle— don't update page titlescroll:top/scroll:bottom— scroll target after swapshow:top/show:bottom— scroll element into viewfocus-scroll:true/false— scroll focused element into view
<button hx-delete="/item" hx-swap="outerHTML swap:1s">Delete with fade</button>
Inherited.
hx-swap-oob — Out of Band Swaps
Allows response elements to update multiple parts of the page simultaneously. Add hx-swap-oob="true" (or a swap strategy) to elements in the response that should be swapped into their matching id location.
<!-- Server response can include: -->
<div id="main-content">Primary content here</div>
<div id="notification-count" hx-swap-oob="true">5</div>
<div id="sidebar" hx-swap-oob="outerHTML">Updated sidebar</div>
Use <template> to wrap elements that can't exist standalone (like <tr>, <td>, <option>):
<template><tr id="row-1" hx-swap-oob="true"><td>Updated</td></tr></template>
Not inherited.
hx-vals — Additional Request Parameters
Add extra values to requests in JSON format:
<button hx-post="/action" hx-vals='{"key": "value"}'>Go</button>
Dynamic values with js: prefix:
<button hx-post="/action" hx-vals='js:{timestamp: Date.now()}'>Go</button>
Inherited. Child values override parent values.
hx-include — Include Extra Inputs
Include input values from other elements:
<button hx-post="/search" hx-include="#search-form">Search</button>
<button hx-post="/save" hx-include="closest form">Save</button>
hx-select and hx-select-oob — Cherry-pick Response Content
hx-select picks a portion of the response to swap. hx-select-oob picks portions for out-of-band swaps elsewhere on the page.
<div hx-get="/page" hx-select="#content" hx-select-oob="#sidebar,#nav">
Both inherited.
Important Patterns
Inheritance
Most htmx attributes inherit from parent elements. Place shared attributes on a container:
<div hx-target="#result" hx-swap="outerHTML" hx-indicator="#spinner">
<button hx-get="/one">One</button>
<button hx-get="/two">Two</button>
</div>
Control with hx-disinherit (disable) or hx-inherit (enable when globally disabled). HTTP verb attributes (hx-get, hx-post, etc.) and hx-trigger are never inherited.
Loading Indicators
<button hx-get="/data" hx-indicator="#spinner">Load</button>
<span id="spinner" class="htmx-indicator">Loading...</span>
htmx adds htmx-request class to the indicator during requests. Default CSS transitions opacity from 0 to 1.
CSS Transitions & Animations
htmx applies CSS classes at different lifecycle stages:
htmx-request— during the request (on the indicator or triggering element)htmx-swapping— during the swap phasehtmx-settling— during the settling phasehtmx-added— on newly added content before settling
Use these with CSS transitions for smooth animations:
tr.htmx-swapping td { opacity: 0; transition: opacity 1s ease-out; }
.htmx-added { opacity: 0; }
#element { transition: opacity 300ms ease-in; }
Boosting
hx-boost="true" progressively enhances links and forms to use AJAX:
<nav hx-boost="true">
<a href="/page1">Page 1</a> <!-- Now uses AJAX -->
<a href="/page2">Page 2</a>
</nav>
Confirmation and Prompts
<button hx-delete="/item/1" hx-confirm="Are you sure?">Delete</button>
<button hx-post="/rename" hx-prompt="Enter new name">Rename</button>
Prompt value is sent in the HX-Prompt request header.
Synchronization
Prevent race conditions between related requests:
<form hx-post="/validate" hx-sync="closest form:abort">
<input hx-post="/validate-email" hx-sync="closest form:abort"/>
<button type="submit">Submit</button>
</form>
Strategies: drop, abort, replace, queue first, queue last, queue all.
Inline Event Handling (hx-on)
<button hx-get="/data" hx-on::after-request="alert('Done!')">Load</button>
<form hx-on::after-request="if(event.detail.successful) this.reset()">
Double-colon :: is shorthand for htmx: prefix. this refers to the element, event is the event object.
Request and Response Headers
Request Headers (sent by htmx)
| Header | Description |
|---|---|
HX-Request |
Always "true" |
HX-Trigger |
ID of the triggered element |
HX-Trigger-Name |
Name of the triggered element |
HX-Target |
ID of the target element |
HX-Current-URL |
Current browser URL |
HX-Boosted |
"true" if boosted |
HX-Prompt |
User response to hx-prompt |
Response Headers (sent by server)
| Header | Description |
|---|---|
HX-Trigger |
Trigger client-side events |
HX-Trigger-After-Swap |
Trigger events after swap |
HX-Trigger-After-Settle |
Trigger events after settle |
HX-Push-Url |
Push URL to browser history |
HX-Replace-Url |
Replace current URL |
HX-Redirect |
Full page redirect |
HX-Location |
Client-side redirect (AJAX) |
HX-Refresh |
Full page refresh |
HX-Reswap |
Override swap strategy |
HX-Retarget |
Override swap target |
HX-Reselect |
Override hx-select |
HX-Trigger header can carry data:
HX-Trigger: {"showMessage": "Item deleted", "itemCount": "5"}
Listen for these events:
<div hx-trigger="showMessage from:body" hx-get="/notifications">
Response Handling
By default, 2xx responses are swapped, 4xx/5xx are not. This is configurable:
<meta name="htmx-config" content='{
"responseHandling": [
{"code": "204", "swap": false},
{"code": "[23]..", "swap": true},
{"code": "422", "swap": true, "error": true},
{"code": "[45]..", "swap": false, "error": true}
]
}'>
A 204 No Content response causes no swap. For DELETE, return 200 with empty body to clear the element.
Security Considerations
htmx.config.selfRequestsOnly(default:true) — restricts AJAX to same domain- Use
hx-disableon containers with user-generated content hx-valswithjs:prefix evaluates JavaScript — sanitize user input- Set CSP nonces via
htmx.config.inlineScriptNonceandhtmx.config.inlineStyleNonce htmx.config.allowEvalcan be set tofalsefor strict CSP
Configuration
Set via meta tag or JavaScript:
<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>
Key config options: defaultSwapStyle (default: innerHTML), defaultSettleDelay (default: 20ms), historyCacheSize (default: 10), selfRequestsOnly (default: true), globalViewTransitions (default: false).
Reference Files
For detailed information, consult these reference files:
references/attributes.md— Complete attribute reference with all options and edge casesreferences/patterns.md— Common UI patterns with full HTML and server-side examplesreferences/extensions.md— SSE, WebSocket, Idiomorph, head-support, preload, response-targets, and building custom extensionsreferences/server-side.md— Server integration patterns, response headers, and framework examples
Source: mintarasss/htmx-skill — distributed by TomeVault.