/SKILL.md
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)
npx skillsauth add lullabot/htmx-expert htmx-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill provides comprehensive guidance for htmx development, the library that extends HTML to access modern browser features directly without JavaScript.
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.
| 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 |
hx-trigger: Customize when requests fire
changed, delay:Xms, throttle:Xms, onceload, revealed, every Xsfrom:<selector>, target:<selector>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-data for file uploads)
hx-target: Where to place response content
this, closest <sel>, next <sel>, previous <sel>, find <sel>hx-swap: How to insert content
innerHTML (default), outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, noneswap:Xms, settle:Xms, scroll:top, show:tophx-select: Select subset of response to swap
hx-select-oob: Select elements for out-of-band swaps
htmx-indicator class)<button hx-get="/api/data"
hx-target="#result"
hx-swap="innerHTML">
Load Data
</button>
<div id="result"></div>
<input type="search"
name="q"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results">
<div id="search-results"></div>
<div hx-get="/items?page=2"
hx-trigger="revealed"
hx-swap="afterend">
Loading more...
</div>
<div hx-get="/status"
hx-trigger="every 5s"
hx-swap="innerHTML">
Status: Unknown
</div>
<form hx-post="/submit"
hx-target="#response"
hx-swap="outerHTML">
<input name="email" type="email" required>
<button type="submit">Submit</button>
</form>
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>
<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 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)
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 |
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')
| 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 |
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';
});
htmx.config.selfRequestsOnly = true;
htmx.config.allowScriptTags = false;
<body hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'>
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}'>
<script src="https://unpkg.com/htmx-ext-<name>@<version>/<name>.js"></script>
<body hx-ext="extension-name">
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>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.
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'));
swap:100ms for transitionsevent.detail for datahtmx will NOT work when opening HTML files directly from the filesystem (file:// URLs). This causes htmx:invalidPath errors because:
file:// URLsSolution: 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
For htmx examples and prototypes, create a simple Python server that:
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()
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>
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).
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>
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.
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')">
Separate triggers with commas:
<div hx-get="/api/data"
hx-trigger="load, every 5s, click from:#refresh-btn">
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>
For detailed reference, consult:
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.