| name | htmx-expert |
| description | This skill should be used when users need help with htmx development, including implementing AJAX interactions, understanding htmx attributes (hx-get, hx-post, hx-swap, hx-target, hx-trigger), debugging htmx behavior, building hypermedia-driven applications, or following htmx best practices. Use when users ask about htmx patterns, server-side HTML responses, or transitioning from SPA frameworks to htmx. (user) |
htmx Expert
This skill provides comprehensive guidance for htmx development, the library that extends HTML to access modern browser features directly without JavaScript.
Core Philosophy
htmx represents a paradigm shift toward hypermedia-first web development. Instead of treating HTML as a presentation layer with JSON APIs, htmx extends HTML to handle AJAX requests, CSS transitions, WebSockets, and Server-Sent Events directly. Servers respond with HTML fragments, not JSON.
When to Use This Skill
- Implementing htmx attributes and interactions
- Building hypermedia-driven applications
- Debugging htmx request/response cycles
- Converting SPA patterns to htmx approaches
- Understanding htmx events and lifecycle
- Configuring htmx extensions
- Implementing proper security measures
Core Attributes Reference
HTTP Verb Attributes
| Attribute | Purpose | Default Trigger |
|---|---|---|
hx-get |
Issue GET request | click |
hx-post |
Issue POST request | click (form: submit) |
hx-put |
Issue PUT request | click |
hx-patch |
Issue PATCH request | click |
hx-delete |
Issue DELETE request | click |
Request Control
hx-trigger: Customize when requests fire
- Modifiers:
changed,delay:Xms,throttle:Xms,once - Special triggers:
load,revealed,every Xs - Extended:
from:<selector>,target:<selector>
- Modifiers:
hx-include: Include additional element values in request
hx-params: Filter which parameters to send (
*,none,not <param>,<param>)hx-headers: Add custom headers (JSON format)
hx-vals: Add values to request (JSON format)
hx-encoding: Set encoding (
multipart/form-datafor file uploads)
Response Handling
hx-target: Where to place response content
- Extended selectors:
this,closest <sel>,next <sel>,previous <sel>,find <sel>
- Extended selectors:
hx-swap: How to insert content
innerHTML(default),outerHTML,beforebegin,afterbegin,beforeend,afterend,delete,none- Modifiers:
swap:Xms,settle:Xms,scroll:top,show:top
hx-select: Select subset of response to swap
hx-select-oob: Select elements for out-of-band swaps
State Management
- hx-push-url: Push URL to browser history
- hx-replace-url: Replace current URL in history
- hx-history: Control history snapshot behavior
- hx-history-elt: Specify element to snapshot
UI Indicators
- hx-indicator: Element to show during request (add
htmx-indicatorclass) - hx-disabled-elt: Elements to disable during request
Security & Control
- hx-confirm: Show confirmation dialog before request
- hx-validate: Enable HTML5 validation on non-form elements
- hx-disable: Disable htmx processing on element and descendants
- hx-sync: Coordinate requests between elements
Implementation Patterns
Basic AJAX Pattern
<button hx-get="/api/data"
hx-target="#result"
hx-swap="innerHTML">
Load Data
</button>
<div id="result"></div>
Active Search
<input type="search"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results">
<div id="search-results"></div>
Infinite Scroll
<div hx-get="/items?page=2"
hx-trigger="revealed"
hx-swap="afterend">
Loading more...
</div>
Polling
<div hx-get="/status"
hx-trigger="every 5s"
hx-swap="innerHTML">
Status: Unknown
</div>
Form Submission
<form hx-post="/submit"
hx-target="#response"
hx-swap="outerHTML">
<input name="email" type="email" required>
<button type="submit">Submit</button>
</form>
Out-of-Band Updates
Server response can update multiple elements:
<!-- Main response -->
<div id="main-content">Updated content</div>
<!-- OOB updates -->
<div id="notification" hx-swap-oob="true">New notification!</div>
<span id="counter" hx-swap-oob="true">42</span>
Loading Indicators
<button hx-get="/slow-endpoint"
hx-indicator="#spinner">
Load
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif">
CSS for indicators:
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator {
opacity: 1;
}
Server Response Patterns
Return HTML Fragments
Server endpoints return HTML, not JSON:
# Flask example
@app.route('/search')
def search():
q = request.args.get('q', '')
results = search_database(q)
return render_template('_search_results.html', results=results)
Response Headers
htmx recognizes special headers:
| Header | Purpose |
|---|---|
HX-Location |
Client-side redirect (with context) |
HX-Push-Url |
Push URL to history |
HX-Redirect |
Full page redirect |
HX-Refresh |
Refresh the page |
HX-Reswap |
Override hx-swap value |
HX-Retarget |
Override hx-target value |
HX-Trigger |
Trigger client-side events |
HX-Trigger-After-Settle |
Trigger after settle |
HX-Trigger-After-Swap |
Trigger after swap |
Detect htmx Requests
Check HX-Request header to differentiate htmx from regular requests:
if request.headers.get('HX-Request'):
return render_template('_partial.html')
else:
return render_template('full_page.html')
Events
Key Events
| Event | When Fired |
|---|---|
htmx:load |
Element loaded into DOM |
htmx:configRequest |
Before request sent (modify params/headers) |
htmx:beforeRequest |
Before AJAX request |
htmx:afterRequest |
After AJAX request completes |
htmx:beforeSwap |
Before content swap |
htmx:afterSwap |
After content swap |
htmx:afterSettle |
After DOM settles |
htmx:confirm |
Before confirmation dialog |
htmx:validation:validate |
Custom validation hook |
Event Handling
Using hx-on*:
<button hx-get="/data"
hx-on:htmx:before-request="console.log('Starting...')"
hx-on:htmx:after-swap="console.log('Done!')">
Load
</button>
Using JavaScript:
document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.headers['X-Custom-Header'] = 'value';
});
Security Best Practices
- Escape All User Content: Prevent XSS through server-side template escaping
- Use hx-disable: Prevent htmx processing on untrusted content
- Restrict Request Origins:
htmx.config.selfRequestsOnly = true; - Disable Script Processing:
htmx.config.allowScriptTags = false; - Include CSRF Tokens:
<body hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'> - Content Security Policy: Layer browser-level protections
Configuration
Key htmx.config options:
htmx.config.defaultSwapStyle = 'innerHTML';
htmx.config.timeout = 0; // Request timeout (0 = none)
htmx.config.historyCacheSize = 10;
htmx.config.globalViewTransitions = false;
htmx.config.scrollBehavior = 'instant'; // or 'smooth', 'auto'
htmx.config.selfRequestsOnly = false;
htmx.config.allowScriptTags = true;
htmx.config.allowEval = true;
Or via meta tag:
<meta name="htmx-config" content='{"selfRequestsOnly":true}'>
Extensions
Loading Extensions
<script src="https://unpkg.com/htmx-ext-<name>@<version>/<name>.js"></script>
<body hx-ext="extension-name">
Common Extensions
- head-support: Merge head tag information across requests
- idiomorph: Morphing swaps (preserves element state)
- sse: Server-Sent Events support
- ws: WebSocket support
- preload: Content preloading
- response-targets: HTTP status-based targeting
Debugging
Enable logging:
htmx.logAll();
Check request headers in Network tab:
HX-Request: trueHX-Target: <target-id>HX-Trigger: <trigger-id>HX-Current-URL: <page-url>
Progressive Enhancement
Structure for graceful degradation:
<form action="/search" method="POST">
<input name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<button type="submit">Search</button>
</form>
<div id="results"></div>
Non-JavaScript users get form submission; JavaScript users get AJAX.
Third-Party Integration
Initialize libraries on htmx-loaded content:
htmx.onLoad(function(content) {
content.querySelectorAll('.datepicker').forEach(el => {
new Datepicker(el);
});
});
For programmatically added htmx content:
htmx.process(document.getElementById('new-content'));
Common Gotchas
- ID Stability: Keep element IDs stable for CSS transitions
- Swap Timing: Default 0ms swap delay; use
swap:100msfor transitions - Event Bubbling: htmx events bubble; use
event.detailfor data - Form Data: Only named inputs are included in requests
- History: History snapshots store innerHTML, not full DOM state
Development Environment Requirements
htmx Requires HTTP (Not file://)
htmx will NOT work when opening HTML files directly from the filesystem (file:// URLs). This causes htmx:invalidPath errors because:
- Browsers block cross-origin requests from
file://URLs - htmx needs to make HTTP requests to endpoints
Solution: Always serve htmx applications via HTTP server:
# Simple Python server (recommended for development)
python3 -m http.server 8000
# Or create a custom server with API endpoints
python3 server.py
Minimal Development Server Pattern
For htmx examples and prototypes, create a simple Python server that:
- Serves static files (HTML, CSS, JS)
- Provides API endpoints that return HTML fragments
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
class HtmxHandler(SimpleHTTPRequestHandler):
def do_GET(self):
path = urlparse(self.path).path
if path.startswith("/api/"):
# Return HTML fragment
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(b"<div>Response HTML</div>")
else:
# Serve static files
super().do_GET()
HTTPServer(("", 8000), HtmxHandler).serve_forever()
Practical Implementation Lessons
Loading Indicators with CSS Spinner
Use CSS-only spinners instead of image files for better performance:
<button hx-get="/api/slow"
hx-indicator="#spinner">
Load
<span id="spinner" class="spinner htmx-indicator"></span>
</button>
<style>
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3d72d7;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Input Search with Proper Trigger
Use input changed instead of keyup changed for better UX (catches paste, autofill):
<input type="search"
name="q"
hx-get="/api/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#results">
The search trigger handles the search input's clear button (X).
Self-Targeting with Polling
For elements that replace themselves (polling), use hx-target="this":
<div hx-get="/api/time"
hx-trigger="load, every 2s"
hx-target="this"
hx-swap="innerHTML">
Loading...
</div>
Row Updates with closest
For list items where each row has its own update button:
<li id="item-1">
<span>Item 1</span>
<button hx-get="/api/update-item/1"
hx-target="closest li"
hx-swap="outerHTML">
Update
</button>
</li>
Server returns complete <li> element with new htmx attributes intact.
Event Attribute Syntax
The hx-on:: syntax uses double colons for htmx events:
<!-- Correct -->
<button hx-on::before-request="console.log('starting')">
<!-- Also correct (older syntax) -->
<button hx-on:htmx:before-request="console.log('starting')">
Combining Multiple Triggers
Separate triggers with commas:
<div hx-get="/api/data"
hx-trigger="load, every 5s, click from:#refresh-btn">
Form POST with Loading State
Combine hx-indicator and hx-disabled-elt for complete UX:
<form hx-post="/api/submit"
hx-target="#result"
hx-indicator="#spinner"
hx-disabled-elt="find button">
<input name="email" required>
<button type="submit">
Submit
<span id="spinner" class="spinner htmx-indicator"></span>
</button>
</form>
Additional Resources
For detailed reference, consult:
- Official docs: https://htmx.org/docs/
- Attributes reference: https://htmx.org/reference/
- Examples: https://htmx.org/examples/