skills/htmx/SKILL.md
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.
npx skillsauth add 1mangesh1/dev-skills-collection htmxInstall 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.
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 -->
<script src="https://unpkg.com/[email protected]"></script>
<!-- Any element can make requests -->
<button hx-get="/api/users" hx-target="#user-list" hx-swap="innerHTML">
Load Users
</button>
<div id="user-list"></div>
<!-- GET -->
<div hx-get="/items">Load Items</div>
<!-- POST -->
<button hx-post="/items" hx-vals='{"name": "New Item"}'>Create</button>
<!-- PUT -->
<button hx-put="/items/42" hx-vals='{"name": "Updated"}'>Update</button>
<!-- PATCH -->
<button hx-patch="/items/42" hx-vals='{"status": "done"}'>Mark Done</button>
<!-- DELETE with confirmation -->
<button hx-delete="/items/42" hx-confirm="Delete this item?">Delete</button>
hx-swap controls how the response HTML replaces content.
<!-- Replace inner content of target (default) -->
<div hx-get="/content" hx-swap="innerHTML">Load</div>
<!-- Replace entire target element -->
<div hx-get="/content" hx-swap="outerHTML">Replace Me</div>
<!-- Insert before target's first child -->
<div hx-get="/new-row" hx-swap="afterbegin">Prepend</div>
<!-- Insert after target's last child -->
<div hx-get="/new-row" hx-swap="beforeend">Append</div>
<!-- Insert before the target element -->
<div hx-get="/sibling" hx-swap="beforebegin">Before</div>
<!-- Insert after the target element -->
<div hx-get="/sibling" hx-swap="afterend">After</div>
<!-- Delete target after request -->
<button hx-delete="/items/42" hx-swap="delete" hx-target="closest tr">Remove</button>
<!-- No swap — fire request but keep DOM unchanged -->
<button hx-post="/track-click" hx-swap="none">Track</button>
<!-- Swap modifiers -->
<div hx-get="/data" hx-swap="innerHTML swap:300ms settle:500ms scroll:top show:top">
Smooth transitions
</div>
hx-target specifies where the response gets placed.
<!-- CSS selector -->
<button hx-get="/users" hx-target="#results">Search</button>
<!-- Relative selectors -->
<button hx-get="/edit" hx-target="closest .card">Edit Card</button>
<button hx-get="/detail" hx-target="find .content">Show Detail</button>
<button hx-get="/next" hx-target="next .panel">Next Panel</button>
<button hx-get="/prev" hx-target="previous .panel">Prev Panel</button>
<!-- this — swap the element itself -->
<div hx-get="/self-update" hx-target="this">Click to reload</div>
<!-- Target the body -->
<a hx-get="/page" hx-target="body">Full page swap</a>
hx-trigger controls when the request fires.
<!-- Default: click for buttons/links, change for inputs, submit for forms -->
<input hx-get="/search" hx-trigger="keyup" hx-target="#results" name="q">
<!-- Modifiers -->
<input hx-get="/search" hx-trigger="keyup changed delay:300ms" name="q">
<input hx-get="/validate" hx-trigger="keyup throttle:500ms" name="email">
<div hx-get="/news" hx-trigger="every 30s">Live feed</div>
<!-- from: — listen to events on other elements -->
<div hx-get="/updates" hx-trigger="click from:body">Refresh on any click</div>
<!-- Multiple triggers -->
<input hx-get="/search" hx-trigger="keyup changed delay:300ms, search" name="q">
<!-- Intersection observer — fires when element enters viewport -->
<div hx-get="/lazy-content" hx-trigger="intersect once">Loading...</div>
<!-- Load trigger — fires on page load -->
<div hx-get="/dashboard-stats" hx-trigger="load">Loading stats...</div>
<!-- Custom events -->
<div hx-get="/refresh" hx-trigger="refreshData from:body">Data</div>
Show loading state during requests.
<button hx-get="/slow-data" hx-indicator="#spinner">
Load Data
<span id="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 -->
<div hx-indicator="closest .card">
<button hx-get="/data">Load</button>
<div class="htmx-indicator">
<svg class="animate-spin h-5 w-5">...</svg>
</div>
</div>
<!-- Disable button during request -->
<button hx-get="/data" hx-disabled-elt="this">Submit</button>
<!-- Disable multiple elements -->
<button hx-post="/save" hx-disabled-elt="closest form">Save</button>
<!-- Forms automatically include all inputs -->
<form hx-post="/contacts" hx-target="#contact-list" hx-swap="beforeend">
<input name="name" required>
<input name="email" type="email" required>
<button type="submit">Add Contact</button>
</form>
<!-- Include inputs from outside the triggering element -->
<input id="search-input" name="q">
<button hx-get="/search" hx-include="#search-input" hx-target="#results">Search</button>
<!-- Include entire form -->
<button hx-post="/save" hx-include="closest form">Save</button>
<!-- Add extra values not in the form -->
<button hx-post="/save" hx-vals='{"source": "web", "version": 2}'>Save</button>
<!-- Dynamic values with JavaScript -->
<button hx-post="/save" hx-vals="js:{ts: Date.now()}">Save with Timestamp</button>
<!-- Control which params are sent -->
<form hx-post="/update" hx-params="*">Send all</form>
<form hx-post="/update" hx-params="none">Send none</form>
<form hx-post="/update" hx-params="name,email">Send specific</form>
<form hx-post="/update" hx-params="not password">Exclude specific</form>
<!-- File upload -->
<form hx-post="/upload" hx-encoding="multipart/form-data">
<input type="file" name="document">
<button>Upload</button>
</form>
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:
<div id="main-content">
<!-- This goes to the normal target -->
<p>Item saved successfully.</p>
</div>
<div id="item-count" hx-swap-oob="true">Total: 43 items</div>
<div id="notification" hx-swap-oob="innerHTML">Saved at 2:30 PM</div>
<tr id="row-42" hx-swap-oob="outerHTML">
<td>Updated Row</td>
</tr>
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
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
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.get('/contacts', (req, res) => {
const isHtmx = req.headers['hx-request'];
const contacts = getContacts(req.query.q);
const html = contacts.map(c =>
`<tr><td>${c.name}</td><td>${c.email}</td></tr>`
).join('');
if (isHtmx) return res.send(html); // return fragment
res.render('contacts', { contacts }); // return full page
});
app.delete('/contacts/:id', (req, res) => {
deleteContact(req.params.id);
res.set('HX-Trigger', 'contactsChanged');
res.send(''); // empty response with delete swap
});
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/search')
def search():
q = request.args.get('q', '')
results = search_contacts(q)
if request.headers.get('HX-Request'):
return render_template('partials/contact_rows.html', contacts=results)
return render_template('search.html', contacts=results)
@app.route('/contacts', methods=['POST'])
def create_contact():
contact = create(request.form)
resp = make_response(render_template('partials/contact_row.html', contact=contact))
resp.headers['HX-Trigger'] = 'contactsChanged'
return resp
from django.http import HttpResponse
from django.template.loader import render_to_string
def contact_list(request):
contacts = Contact.objects.filter(name__icontains=request.GET.get('q', ''))
if request.headers.get('HX-Request'):
html = render_to_string('partials/rows.html', {'contacts': contacts})
return HttpResponse(html)
return render(request, 'contacts.html', {'contacts': contacts})
func handleSearch(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query().Get("q")
results := searchContacts(q)
if r.Header.Get("HX-Request") != "" {
tmpl.ExecuteTemplate(w, "contact-rows", results)
return
}
tmpl.ExecuteTemplate(w, "full-page", results)
}
<input type="search" name="q"
hx-get="/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#search-results"
hx-indicator="#search-spinner"
placeholder="Search contacts...">
<span id="search-spinner" class="htmx-indicator">Searching...</span>
<table><tbody id="search-results"></tbody></table>
<table><tbody id="results">
<!-- rows here -->
<tr hx-get="/contacts?page=2"
hx-trigger="revealed"
hx-swap="afterend"
hx-select="tbody > tr">
<td>Loading more...</td>
</tr>
</tbody></table>
<!-- Display mode -->
<div hx-get="/contacts/42/edit" hx-trigger="click" hx-swap="outerHTML">
<p>John Doe — [email protected]</p>
</div>
<!-- Server returns edit form -->
<form hx-put="/contacts/42" hx-swap="outerHTML">
<input name="name" value="John Doe">
<input name="email" value="[email protected]">
<button>Save</button>
<button hx-get="/contacts/42" hx-swap="outerHTML">Cancel</button>
</form>
<form hx-put="/contacts/bulk" hx-target="#table-body" hx-swap="innerHTML">
<input type="checkbox" id="select-all"
onclick="document.querySelectorAll('.row-check').forEach(c => c.checked = this.checked)">
<table><tbody id="table-body">
<tr>
<td><input type="checkbox" class="row-check" name="ids" value="1"></td>
<td>Contact 1</td>
</tr>
</tbody></table>
<button>Activate Selected</button>
</form>
<div hx-get="/dashboard/chart" hx-trigger="load" hx-swap="outerHTML">
<div class="skeleton-loader" style="height: 300px;"></div>
</div>
<tr>
<td>Item Name</td>
<td>
<button hx-delete="/items/42"
hx-target="closest tr"
hx-swap="outerHTML swap:500ms"
hx-confirm="Delete this item?">
Delete
</button>
</td>
</tr>
<style>
tr.htmx-swapping { opacity: 0; transition: opacity 500ms ease-out; }
</style>
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 -->
<div hx-boost="true">
<a href="/about">About</a> <!-- becomes hx-get="/about" -->
<a href="/contact">Contact</a>
<form action="/search" method="get"> <!-- becomes hx-get="/search" -->
<input name="q">
<button>Search</button>
</form>
</div>
<!-- Boost the entire body for SPA-like navigation -->
<body hx-boost="true">
<!-- All navigation is now AJAX -->
</body>
<!-- Exclude specific links -->
<a href="/download.pdf" hx-boost="false">Download PDF</a>
<!-- Push URL to history (default with boost) -->
<a hx-get="/page" hx-push-url="true">Navigate</a>
<a hx-get="/modal" hx-push-url="false">Open Modal</a>
<!-- Load extension -->
<script src="https://unpkg.com/[email protected]/ws.js"></script>
<!-- WebSocket -->
<div hx-ext="ws" ws-connect="/ws/chat">
<div id="chat-messages"></div>
<form ws-send>
<input name="message">
<button>Send</button>
</form>
</div>
<!-- Server-Sent Events -->
<script src="https://unpkg.com/[email protected]/sse.js"></script>
<div hx-ext="sse" sse-connect="/events">
<div sse-swap="notification">Waiting for notifications...</div>
<div sse-swap="status">Status: unknown</div>
</div>
Alpine handles client-side state; htmx handles server communication.
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div x-data="{ open: false }">
<button @click="open = !open">Toggle</button>
<div x-show="open" x-transition>
<div hx-get="/panel-content" hx-trigger="intersect once" hx-swap="innerHTML">
Loading...
</div>
</div>
</div>
<!-- Alpine reacts to htmx events -->
<div x-data="{ saving: false }"
@htmx:before-request.window="saving = true"
@htmx:after-request.window="saving = false">
<span x-show="saving">Saving...</span>
<form hx-post="/save">
<input name="data">
<button>Save</button>
</form>
</div>
htmx adds classes during the swap lifecycle for animation hooks.
/* Element being removed */
.htmx-swapping { opacity: 0; transition: opacity 0.5s ease-out; }
/* New content settling in */
.htmx-added { opacity: 0; }
.htmx-settling { opacity: 1; transition: opacity 0.3s ease-in; }
/* During request */
.htmx-request { opacity: 0.5; }
<!-- Use swap/settle timing to match CSS transitions -->
<div hx-get="/new-content" hx-swap="innerHTML swap:500ms settle:300ms">
Animated swap
</div>
<!-- View Transitions API (modern browsers) -->
<div hx-get="/page" hx-swap="innerHTML transition:true">Navigate</div>
<!-- Inline field validation -->
<input name="email" type="email"
hx-post="/validate/email"
hx-trigger="blur changed"
hx-target="next .error"
hx-swap="innerHTML">
<span class="error"></span>
<!-- Server returns validation HTML -->
<!-- Success: empty string or checkmark -->
<!-- Failure: <span class="text-red-500">Email already taken</span> -->
<!-- Form-level validation with error summary -->
<form hx-post="/register" hx-target="#form-errors" hx-swap="innerHTML">
<div id="form-errors"></div>
<input name="username" required>
<input name="email" type="email" required>
<button>Register</button>
</form>
<!-- Prevent request if client validation fails -->
<form hx-post="/save"
hx-trigger="submit"
hx-on::before-request="if(!this.checkValidity()){event.preventDefault();this.reportValidity()}">
<input name="name" required>
<button>Save</button>
</form>
<!-- Listen to htmx events -->
<div hx-get="/data" hx-trigger="load"
hx-on::after-settle="console.log('Content loaded')">
Loading...
</div>
<!-- JavaScript event listeners -->
<script>
document.body.addEventListener('htmx:beforeRequest', (e) => {
console.log('Request starting:', e.detail.pathInfo);
});
document.body.addEventListener('htmx:afterSwap', (e) => {
console.log('Content swapped into:', e.detail.target);
});
document.body.addEventListener('htmx:responseError', (e) => {
alert('Request failed: ' + e.detail.xhr.status);
});
// Respond to server-triggered events via HX-Trigger header
document.body.addEventListener('showToast', (e) => {
showNotification(e.detail.message);
});
</script>
Choose htmx when:
Choose React/Vue/Svelte when:
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.
tools
Parallel execution with xargs, GNU parallel, and batch processing patterns. Use when user mentions "xargs", "parallel", "batch processing", "run in parallel", "parallel execution", "process list of files", "bulk operations", "concurrent commands", "map over files", or running commands on multiple inputs.
development
WebSocket implementation for real-time bidirectional communication. Use when user mentions "websocket", "ws://", "wss://", "real-time", "live updates", "chat application", "socket.io", "Server-Sent Events", "SSE", "push notifications", "live data", "streaming data", "bidirectional communication", "websocket server", "reconnection", or building real-time features.
tools
Frontend bundler configuration for Webpack and Vite. Use when user mentions "webpack", "vite", "bundler", "vite config", "webpack config", "code splitting", "tree shaking", "hot module replacement", "HMR", "build optimization", "bundle size", "chunk splitting", "loader", "plugin", "esbuild", "rollup", "dev server", or configuring JavaScript build tools.
tools
VS Code configuration, extensions, keybindings, and workspace optimization. Use when user mentions "vscode", "vs code", "vscode settings", "vscode extensions", "keybindings", "code editor", "workspace settings", "settings.json", "launch.json", "tasks.json", "vscode snippets", "devcontainer", "remote development", or customizing their VS Code setup.