Use when building server-driven UIs with htmx, designing hx-* attribute compositions, choosing swap strategies (innerHTML/outerHTML/morph), wiring out-of-band updates, integrating with form validation, or migrating from a SPA back to server-rendered HTML. Triggers: hx-get/hx-post setup, hx-target + hx-swap interplay, hx-trigger debouncing, OOB swaps for parts of the page outside the request target, response headers like HX-Trigger and HX-Push-Url, integration with Hyperscript or Alpine.js, accessibility considerations on partial updates. NOT for full SPAs (use React/Vue), framework-specific server libraries (use those skills directly), or non-HTML responses.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Use when building server-driven UIs with htmx, designing hx-* attribute compositions, choosing swap strategies (innerHTML/outerHTML/morph), wiring out-of-band updates, integrating with form validation, or migrating from a SPA back to server-rendered HTML. Triggers: hx-get/hx-post setup, hx-target + hx-swap interplay, hx-trigger debouncing, OOB swaps for parts of the page outside the request target, response headers like HX-Trigger and HX-Push-Url, integration with Hyperscript or Alpine.js, accessibility considerations on partial updates. NOT for full SPAs (use React/Vue), framework-specific server libraries (use those skills directly), or non-HTML responses.
metadata
{"category":"Frontend & UI","tags":["htmx","hypermedia","progressive-enhancement","server-rendering","hateoas"],"provenance":{"kind":"first-party","owners":["port-daddy"]},"pairs-with":[{"skill":"hono-patterns","reason":"A lightweight Hono backend returning HTML fragments (branching on the HX-Request header) is the natural server half of an htmx UI"},{"skill":"web-design-expert","reason":"htmx swaps in whatever the server renders; the layout, typography, and interaction design of those fragments live there"},{"skill":"websocket-streaming","reason":"The hx-ext ws/sse extensions ride on server push; connection lifecycle and streaming design come from the paired skill"}],"io-contract":{"kind":"deliverable","consumes":["[Truncated]","[Truncated]"],"produces":["[Truncated]","[Truncated]"]}}
htmx Progressive Enhancement
htmx adds AJAX, CSS transitions, WebSockets, and server-sent events to HTML via attributes. The mental model is "the server returns HTML; the client swaps it in." For most CRUD apps, this beats a SPA in time-to-ship and bundle size.
When to use
Server-rendered app where SPA complexity isn't paying off.
Adding partial updates to a Rails/Django/Laravel/Phoenix/Hono app.
Form-heavy UIs (admin panels, dashboards) without React's overhead.
Migrating away from a SPA that grew past its complexity budget.
Real-time updates via SSE or WebSockets without a frontend framework.
Click → GET /api/widget → response HTML replaces #out content.
Common attributes
Attribute
Purpose
hx-get / hx-post / hx-put / hx-delete / hx-patch
HTTP method + URL.
hx-target
CSS selector for the swap. Default: the element itself. closest, next, previous selectors supported.
hx-swap
How to swap. innerHTML (default), outerHTML, beforebegin, afterbegin, beforeend, afterend, delete, none.
hx-trigger
When to fire. click (default for buttons), change, keyup changed delay:500ms, revealed, every 5s.
hx-include
Extra fields to include with the request.
hx-vals
Static or JS-computed extra params.
hx-confirm
Browser confirm() before request.
hx-indicator
CSS selector to mark "request in flight."
hx-disabled-elt
Disable elements while request runs.
hx-push-url
Update browser URL on success.
Swap strategies
<!-- Replace contents of target --><ahx-get="/feed/items"hx-target="#list"hx-swap="innerHTML">Refresh</a><!-- Replace the target itself --><buttonhx-delete="/items/42"hx-target="closest tr"hx-swap="outerHTML swap:300ms">
Delete
</button><!-- Append to a list (newest at top) --><formhx-post="/comments"hx-target="#comments"hx-swap="afterbegin">
...
</form>
swap:300ms is a CSS transition delay — the old content gets htmx-swapping class for 300ms, you animate it out, then htmx swaps.
The form sends JSON on submit; server echoes HTML back, swapping into the page.
Anti-patterns
Returning JSON when htmx wanted HTML
Symptom: Endpoint returns 200 but nothing changes on the page.
Diagnosis: htmx swaps response bodies as HTML. JSON ends up rendered as text.
Fix: Server returns HTML fragments for htmx requests (detect via HX-Request header). Keep JSON endpoints separate.
Tracking state on the client
Symptom: Race conditions; page shows stale data after multiple actions.
Diagnosis: Trying to maintain client-side state in JS variables.
Fix: Server is the source of truth. Each response includes the new state's HTML. If you're tempted to maintain client state, you probably want a SPA.
Forgotten hx-target for forms
Symptom: Form submission replaces the form's container instead of updating output area.
Diagnosis:hx-target defaults to the element itself; for forms, this is the form.
Fix: Add hx-target="#output" and hx-swap explicitly.
Missing CSRF token
Symptom: POST/PUT/DELETE returns 403 from a framework that enforces CSRF.
Diagnosis: Form helpers usually inject CSRF; htmx requests don't unless you tell them.
Fix: Use hx-headers='{"X-CSRF-Token": "..."}' or include in hx-vals. Some frameworks have htmx-aware CSRF middleware.
No fallback for JS-disabled
Symptom: Site unusable without JS even though it could degrade.
Diagnosis: Forms and links that only work via htmx.
Fix: Use hx-boost="true" on links and forms — without JS they're regular HTML; with JS they upgrade. Combine action="/path" with hx-post="/path" so POST works either way.
Accessibility: focus + announcements
Symptom: Screen reader users don't know content updated.
Diagnosis: Partial swaps don't trigger ARIA live regions automatically.
Fix: Mark dynamic regions with aria-live="polite". Manage focus on swap with hx-on::after-swap. Add visually-hidden announcements for important changes.
Quality gates
htmx request endpoints return HTML, not JSON.
CSRF tokens included on every state-changing request.
Forms work without JS (progressive enhancement, not JS-required).
Dynamic regions have aria-live for screen readers.
Focus managed on swap (hx-on::after-swap).
No client-side state beyond htmx attributes; server is the source of truth.
Loading indicators on every request that takes >300ms.
OOB swaps used for cross-region updates instead of multiple requests.
Trigger debouncing (delay:) on every type-as-you-search input.
Deterministic Audit
Before shipping (or reviewing) an htmx UI, write its shape as a JSON plan matching
schemas/htmx-progressive-enhancement-plan.schema.json and run it through the
deterministic auditor:
auditHtmxProgressiveEnhancement(plan) (in scripts/htmx_progressive_enhancement_audit.mjs)
turns this skill's anti-patterns and Quality Gates into machine-checkable rules over
structured fields — no keyword matching: JSON returned to an htmx swap (the
nothing-happens bug), state-changing requests without a CSRF token, a UI that dies with JS
disabled, dynamic regions with no aria-live, unmanaged focus after swaps, client-side
state stores shadowing the server, undebounced type-ahead search, and multiple requests
where one OOB swap would do. It returns { pass, score, findings, recommendations } and
exits 1 on failure. examples/sample-input.json is a correctly enhanced, accessible plan
(pass: true, zero findings). See CHANGELOG.md for the bundle's history.
NOT for
Full SPAs with rich client state — htmx adds friction for that case.
Framework-specific server-side rendering — use the framework's idioms (Phoenix LiveView, Rails Hotwire, Laravel Livewire are similar but not htmx).
Native mobile — htmx is web-only.
Heavy real-time collaborative apps (Figma-like) — the round-trip cost is too high.