htmx
HTMX Skill
Provides guidance for building dynamic, interactive web applications using htmx - a library that enables modern browser features directly from HTML attributes.
Overview
htmx extends HTML with attributes that allow any element to issue HTTP requests and update page content without writing JavaScript. It brings the power of AJAX, CSS Transitions, WebSockets, and Server-Sent Events directly into HTML markup.
Core Philosophy: Server returns HTML (not JSON), keeping you in the hypermedia/HATEOAS model. Any element can issue requests, not just anchors and forms. Any event can trigger requests, not just clicks and submissions.
Core Concepts
AJAX Attributes
Use these attributes to issue HTTP requests:
hx-get="/url"- Issues GET requesthx-post="/url"- Issues POST requesthx-put="/url"- Issues PUT requesthx-patch="/url"- Issues PATCH requesthx-delete="/url"- Issues DELETE request
<button hx-post="/clicked" hx-target="#result">
Click Me!
</button>
<div id="result"></div>
Triggering Requests
Control when requests fire with hx-trigger:
Default triggers: input/textarea/select use change, form uses submit, everything else uses click
Custom triggers:
<!-- Trigger on mouseenter -->
<div hx-get="/data" hx-trigger="mouseenter">Hover me</div>
<!-- Trigger on keyup with delay -->
<input hx-get="/search" hx-trigger="keyup changed delay:500ms">
<!-- Multiple triggers -->
<div hx-get="/data" hx-trigger="mouseenter, focus">
Modifiers:
once- Only trigger oncechanged- Only if value changeddelay:500ms- Wait before issuing requestthrottle:1s- Rate limit requestsfrom:<selector>- Listen on different element
Filters:
<!-- Only trigger if Ctrl key pressed -->
<div hx-get="/clicked" hx-trigger="click[ctrlKey]">Ctrl+Click</div>
Special events:
load- Fires when element loadsrevealed- Fires when scrolled into viewportevery 2s- Poll every 2 seconds
Targeting and Swapping
Control where and how content is inserted:
Target selection with hx-target:
<button hx-get="/data" hx-target="#result">Load</button>
<div id="result"></div>
Extended selectors:
this- The element itselfclosest <selector>- Nearest ancestor matching selectornext <selector>- Next sibling matching selectorprevious <selector>- Previous sibling matching selectorfind <selector>- First child descendant
Swap strategies with hx-swap:
innerHTML(default) - Replace inner contentouterHTML- Replace entire elementafterbegin- Prepend inside targetbeforebegin- Insert before targetbeforeend- Append inside targetafterend- Insert after targetdelete- Delete target regardless of responsenone- Don't swap content
Swap modifiers:
<button hx-get="/data" hx-swap="innerHTML swap:100ms settle:200ms">
Request Indicators
Show loading state during requests:
<button hx-get="/slow">
Click Me!
<img class="htmx-indicator" src="/spinner.gif">
</button>
The htmx-indicator class has opacity:0 by default. When request starts, htmx-request class is added to the element, which makes indicators visible.
Specify custom indicator target:
<button hx-get="/data" hx-indicator="#loading">Load</button>
<div id="loading" class="htmx-indicator">Loading...</div>
Common Patterns
Active Search
<input type="text" name="q"
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
placeholder="Search...">
<div id="search-results"></div>
Infinite Scroll
<div hx-get="/more-items"
hx-trigger="revealed"
hx-swap="afterend">
Load More...
</div>
Click to Edit
<div hx-get="/edit/123" hx-target="this" hx-swap="outerHTML">
<label>Name:</label> John Doe
</div>
Delete with Confirmation
<button hx-delete="/item/123"
hx-confirm="Are you sure?"
hx-target="closest tr"
hx-swap="outerHTML swap:1s">
Delete
</button>
Out-of-Band Swaps
Update multiple parts of the page from one response:
<!-- Response HTML -->
<div id="main-content">Main update</div>
<div id="notification" hx-swap-oob="true">
New notification!
</div>
The element with hx-swap-oob="true" swaps into its matching ID anywhere on the page.
Form Handling
Basic Form Submission
<form hx-post="/submit" hx-target="#result">
<input name="email" type="email">
<button type="submit">Submit</button>
</form>
Including Additional Values
<!-- Include other elements -->
<button hx-post="/save"
hx-include="[name='email']">
Save
</button>
<!-- Add extra values -->
<button hx-post="/save"
hx-vals='{"priority": "high"}'>
Save
</button>
File Upload
<form hx-post="/upload"
hx-encoding="multipart/form-data"
hx-target="#result">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
Listen for upload progress:
htmx.on('htmx:xhr:progress', function(evt) {
htmx.find('#progress').value = evt.detail.loaded/evt.detail.total * 100;
});
Request Synchronization
Coordinate requests between elements with hx-sync:
<form hx-post="/store">
<input name="title"
hx-post="/validate"
hx-trigger="change"
hx-sync="closest form:abort">
<button type="submit">Submit</button>
</form>
Strategies:
drop- Drop this request if target is in flightabort- Abort target request if this triggersreplace- Abort target and issue this requestqueue- Queue this request after target
Boosting
Progressive enhancement for regular links and forms:
<div hx-boost="true">
<a href="/page1">Page 1</a>
<a href="/page2">Page 2</a>
</div>
Links and forms become AJAX requests that target the body. Works without JavaScript enabled (graceful degradation).
History Support
Add URLs to browser history:
<a hx-get="/blog" hx-push-url="true">Blog</a>
When user clicks back button, htmx restores the previous state. For history to work, URLs must return complete pages when visited directly.
Disable history caching for sensitive data:
<div hx-history="false">Sensitive content</div>
Headers
Request Headers
htmx automatically sends:
HX-Request: true- Identifies htmx requestsHX-Trigger- ID of triggering elementHX-Target- ID of target elementHX-Current-URL- Current page URLHX-Prompt- User response to prompt
Use these to return partial HTML vs full pages:
if request.headers.get('HX-Request'):
return render_template('partial.html')
return render_template('full_page.html')
Response Headers
Control client behavior from server:
HX-Trigger- Trigger client-side eventsHX-Redirect- Client-side redirect (full page)HX-Location- Client-side redirect (AJAX)HX-Refresh- Force page refreshHX-Retarget- Change target elementHX-Reswap- Change swap strategy
response.headers['HX-Trigger'] = 'itemUpdated'
response.headers['HX-Trigger'] = '{"showMessage": "Saved!"}'
Validation
htmx integrates with HTML5 validation:
<form hx-post="/submit">
<input name="email" type="email" required>
<button type="submit">Submit</button>
</form>
Set htmx.config.reportValidityOfForms = true to show validation messages.
Custom validation:
htmx.on('htmx:validation:validate', function(evt) {
if (evt.target.value === 'forbidden') {
evt.target.setCustomValidity('This value is forbidden');
evt.detail.valid = false;
}
});
Events and Scripting
Event Handling
Use hx-on for inline event handlers:
<button hx-get="/data"
hx-on::before-request="this.classList.add('loading')"
hx-on::after-request="this.classList.remove('loading')">
Load
</button>
JavaScript API
// Trigger requests programmatically
htmx.ajax('GET', '/data', '#target');
// Listen to events
htmx.on('htmx:afterSwap', function(evt) {
console.log('Content swapped');
});
// Process new content
htmx.process(document.body);
// Trigger events
htmx.trigger('#element', 'myEvent', {detail: {foo: 'bar'}});
CSS Transitions
Keep element IDs stable across swaps for automatic transitions:
<!-- Before -->
<div id="content">Old content</div>
<!-- After (same ID) -->
<div id="content" class="highlight">New content</div>
.highlight {
background-color: yellow;
transition: background-color 1s ease-in;
}
htmx preserves the DOM element and transitions the class change.
Configuration
Configure globally or via meta tag:
<meta name="htmx-config" content='{
"defaultSwapStyle": "outerHTML",
"defaultSwapDelay": 100,
"defaultSettleDelay": 200,
"historyCacheSize": 20
}'>
Or in JavaScript:
htmx.config.defaultSwapStyle = 'outerHTML';
htmx.config.timeout = 5000; // 5 second timeout
Installation
CDN (Recommended for quick start)
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
npm
npm install htmx.org@2.0.8
Then import:
import 'htmx.org';
Download
Download from jsDelivr and include locally:
<script src="/js/htmx.min.js"></script>
Best Practices
- Keep IDs stable - Use consistent IDs across requests for CSS transitions
- Return appropriate content - Return partials for htmx requests, full pages for direct access
- Use semantic HTML - htmx enhances HTML, so start with good markup
- Progressive enhancement - Use
hx-boostso features work without JavaScript - Handle errors - Listen to
htmx:responseErrorandhtmx:sendErrorevents - Validate inputs - Enable
htmx.config.reportValidityOfForms = true - Test without JavaScript - Ensure core functionality works when JS is disabled
Debugging
Enable logging:
htmx.logAll();
Or set custom logger:
htmx.logger = function(elt, event, data) {
if(console) {
console.log(event, elt, data);
}
}
Use browser DevTools to inspect:
- Network tab for request/response details
HX-*headers in request/response- Event listeners on elements
htmx-*classes during swap lifecycle
Additional Resources
For comprehensive details:
references/attributes.md- Complete attribute referencereferences/events.md- All htmx events and lifecyclereferences/examples.md- Advanced patterns and real-world examplesreferences/server-side.md- Server-side implementation patterns