htmx for building dynamic web UIs with HTML-over-the-wire. Use when user mentions "htmx", "hx-get", "hx-post", "hx-swap", "hx-trigger", "hypermedia", "HTML over the wire", "server-driven UI", "no JavaScript framework", "htmx boost", "progressive enhancement", "hyperscript", "alpine.js with htmx", or building interactive web pages without heavy JavaScript frameworks.
htmx for building dynamic web UIs with HTML-over-the-wire. Use when user mentions "htmx", "hx-get", "hx-post", "hx-swap", "hx-trigger", "hypermedia", "HTML over the wire", "server-driven UI", "no JavaScript framework", "htmx boost", "progressive enhancement", "hyperscript", "alpine.js with htmx", or building interactive web pages without heavy JavaScript frameworks.
htmx
Fundamentals
htmx gives HTML attributes superpowers: any element can issue HTTP requests, and the server returns HTML fragments that get swapped into the DOM. No JSON APIs, no client-side rendering, no build step. The server is the single source of truth.
<!-- Include htmx --><scriptsrc="https://unpkg.com/htmx.org@2.0.4"></script><!-- Any element can make requests --><buttonhx-get="/api/users"hx-target="#user-list"hx-swap="innerHTML">
Load Users
</button><divid="user-list"></div>
Core Request Attributes
<!-- GET --><divhx-get=>Load Items
Create
Update
Mark Done
Delete
"/items"
</div>
<!-- POST -->
<buttonhx-post="/items"hx-vals='{"name": "New Item"}'>
<buttonhx-delete="/items/42"hx-confirm="Delete this item?">
</button>
Swap Strategies
hx-swap controls how the response HTML replaces content.
<!-- Replace inner content of target (default) --><divhx-get="/content"hx-swap="innerHTML">Load</div><!-- Replace entire target element --><divhx-get="/content"hx-swap="outerHTML">Replace Me</div><!-- Insert before target's first child --><divhx-get="/new-row"hx-swap="afterbegin">Prepend</div><!-- Insert after target's last child --><divhx-get="/new-row"hx-swap="beforeend">Append</div><!-- Insert before the target element --><divhx-get="/sibling"hx-swap="beforebegin">Before</div><!-- Insert after the target element --><divhx-get="/sibling"hx-swap="afterend">After</div><!-- Delete target after request --><buttonhx-delete="/items/42"hx-swap="delete"hx-target="closest tr">Remove</button><!-- No swap — fire request but keep DOM unchanged --><buttonhx-post="/track-click"hx-swap="none">Track</button><!-- Swap modifiers --><divhx-get="/data"hx-swap="innerHTML swap:300ms settle:500ms scroll:top show:top">
Smooth transitions
</div>
Targets
hx-target specifies where the response gets placed.
<!-- CSS selector --><buttonhx-get="/users"hx-target="#results">Search</button><!-- Relative selectors --><buttonhx-get="/edit"hx-target="closest .card">Edit Card</button><buttonhx-get="/detail"hx-target="find .content">Show Detail</button><buttonhx-get="/next"hx-target="next .panel">Next Panel</button><buttonhx-get="/prev"hx-target="previous .panel">Prev Panel</button><!-- this — swap the element itself --><divhx-get="/self-update"hx-target="this">Click to reload</div><!-- Target the body --><ahx-get="/page"hx-target="body">Full page swap</a>
Triggers
hx-trigger controls when the request fires.
<!-- Default: click for buttons/links, change for inputs, submit for forms --><inputhx-get="/search"hx-trigger="keyup"hx-target="#results"name="q"><!-- Modifiers --><inputhx-get="/search"hx-trigger="keyup changed delay:300ms"name="q"><inputhx-get="/validate"hx-trigger="keyup throttle:500ms"name="email"><divhx-get="/news"hx-trigger="every 30s">Live feed</div><!-- from: — listen to events on other elements --><divhx-get="/updates"hx-trigger="click from:body">Refresh on any click</div><!-- Multiple triggers --><inputhx-get="/search"hx-trigger="keyup changed delay:300ms, search"name="q"><!-- Intersection observer — fires when element enters viewport --><divhx-get="/lazy-content"hx-trigger="intersect once">Loading...</div><!-- Load trigger — fires on page load --><divhx-get="/dashboard-stats"hx-trigger="load">Loading stats...</div><!-- Custom events --><divhx-get="/refresh"hx-trigger="refreshData from:body">Data</div>
Indicators
Show loading state during requests.
<buttonhx-get="/slow-data"hx-indicator="#spinner">
Load Data
<spanid="spinner"class="htmx-indicator">Loading...</span></button><style>.htmx-indicator { display: none; }
.htmx-request.htmx-indicator { display: inline; }
.htmx-request.htmx-indicator { display: inline; }
</style><!-- Indicator on parent --><divhx-indicator="closest .card"><buttonhx-get="/data">Load</button><divclass="htmx-indicator"><svgclass="animate-spin h-5 w-5">...</svg></div></div><!-- Disable button during request --><buttonhx-get="/data"hx-disabled-elt="this">Submit</button><!-- Disable multiple elements --><buttonhx-post="/save"hx-disabled-elt="closest form">Save</button>
Form Handling
<!-- Forms automatically include all inputs --><formhx-post="/contacts"hx-target="#contact-list"hx-swap="beforeend"><inputname="name"required><inputname="email"type="email"required><buttontype="submit">Add Contact</button></form><!-- Include inputs from outside the triggering element --><inputid="search-input"name="q"><buttonhx-get="/search"hx-include="#search-input"hx-target="#results">Search</button><!-- Include entire form --><buttonhx-post="/save"hx-include="closest form">Save</button><!-- Add extra values not in the form --><buttonhx-post="/save"hx-vals='{"source": "web", "version": 2}'>Save</button><!-- Dynamic values with JavaScript --><buttonhx-post="/save"hx-vals="js:{ts: Date.now()}">Save with Timestamp</button><!-- Control which params are sent --><formhx-post="/update"hx-params="*">Send all</form><formhx-post="/update"hx-params="none">Send none</form><formhx-post="/update"hx-params="name,email">Send specific</form><formhx-post="/update"hx-params="not password">Exclude specific</form><!-- File upload --><formhx-post="/upload"hx-encoding="multipart/form-data"><inputtype="file"name="document"><button>Upload</button></form>
Out-of-Band Swaps
Update multiple parts of the page from a single response.
<!-- Server response can include out-of-band swaps --><!-- Main response gets swapped into target as usual --><!-- Elements with hx-swap-oob get swapped into matching IDs -->
Server returns:
<divid="main-content"><!-- This goes to the normal target --><p>Item saved successfully.</p></div><divid="item-count"hx-swap-oob="true">Total: 43 items</div><divid="notification"hx-swap-oob="innerHTML">Saved at 2:30 PM</div><trid="row-42"hx-swap-oob="outerHTML"><td>Updated Row</td></tr>
Headers
Request Headers (sent by htmx)
HX-Request: true — always sent, use to detect htmx requests
HX-Target: element-id — id of the target element
HX-Trigger: element-id — id of the triggered element
HX-Trigger-Name: name-attr — name attribute of the trigger
HX-Current-URL: url — current URL of the browser
HX-Prompt: value — user response from hx-prompt
HX-Boosted: true — if request is via hx-boost
Response Headers (sent by server)
HX-Redirect: /new-url — client-side redirect
HX-Refresh: true — full page refresh
HX-Retarget: #new-target — change the target element
HX-Reswap: outerHTML — change the swap strategy
HX-Trigger: eventName — trigger client-side event after settle
HX-Trigger: {"showToast": {"message": "Saved!"}} — trigger with detail
HX-Push-Url: /new-url — push URL to browser history
<table><tbodyid="results"><!-- rows here --><trhx-get="/contacts?page=2"hx-trigger="revealed"hx-swap="afterend"hx-select="tbody > tr"><td>Loading more...</td></tr></tbody></table>
Click to Edit
<!-- Display mode --><divhx-get="/contacts/42/edit"hx-trigger="click"hx-swap="outerHTML"><p>John Doe — john@example.com</p></div><!-- Server returns edit form --><formhx-put="/contacts/42"hx-swap="outerHTML"><inputname="name"value="John Doe"><inputname="email"value="john@example.com"><button>Save</button><buttonhx-get="/contacts/42"hx-swap="outerHTML">Cancel</button></form>
hx-boost converts standard links and forms into AJAX requests with history support. Drop-in progressive enhancement.
<!-- Boost all links and forms in this container --><divhx-boost="true"><ahref="/about">About</a><!-- becomes hx-get="/about" --><ahref="/contact">Contact</a><formaction="/search"method="get"><!-- becomes hx-get="/search" --><inputname="q"><button>Search</button></form></div><!-- Boost the entire body for SPA-like navigation --><bodyhx-boost="true"><!-- All navigation is now AJAX --></body><!-- Exclude specific links --><ahref="/download.pdf"hx-boost="false">Download PDF</a><!-- Push URL to history (default with boost) --><ahx-get="/page"hx-push-url="true">Navigate</a><ahx-get="/modal"hx-push-url="false">Open Modal</a>
Need for native mobile apps via React Native or similar
Hybrid approach: Use htmx for most pages, embed React/Vue components for complex widgets. htmx handles navigation and data mutations; JS frameworks handle rich interactivity.