<!-- skill: HTMX Pages | version: 1.0.0 | description: HTMX hypermedia swaps, triggers and any-backend pairing | category: Web -->
## Instructions
- Core mental model: hypermedia — server returns HTML fragments, HTMX swaps them in; `hx-get/hx-post` + `hx-target` (css selector) + `hx-swap` (`innerHTML/outerHTML/beforebegin/afterend/none`); no client JSON rendering.
- Triggers: `hx-trigger="click, keyup changed delay:300ms, every 10s, load, revealed"`; `changed` + `delay` for search-as-you-type, `revealed` for infinite scroll, `load` for lazy widgets.
- Targets/swaps: `hx-target="#list" hx-swap="innerHTML"` default; `hx-target="closest tr" hx-swap="outerHTML swap:300ms"` row delete; `hx-select="#item"` to pluck fragment from full page response.
- Out-of-band: `hx-swap-oob="true"` elements in response update elsewhere (cart badge + toast + list in one POST); ids must match; use for cross-region updates without extra requests.
- History: `hx-push-url="true"` on nav swaps so back-button works; `hx-boost="true"` on `<body>` upgrades all links/forms to AJAX; server sends full page for no-JS fallback.
- Forms: plain `<form hx-post="/contacts" hx-target="#err">` with server 422 returning error HTML; `hx-include="[name=q]"` merges extra inputs; `hx-vals='{"sort":"asc"}'` adds constants.
- Indicators: `hx-indicator="#spinner"` toggling `.htmx-request` class, `disabled` on submit via CSS `.htmx-request .btn{opacity:.5}`; `hx-confirm="Delete?"` for destructive, `hx-disable` to opt out.
- Headers/params: server reads `HX-Request/HX-Target/HX-Trigger` to return fragment vs full page; client sends `HX-Current-URL`; CSRF via `htmx:configRequest` injecting `X-CSRFToken` from meta/cookie.
- Extensions: `idiomorph` morph swaps preserving focus/video, `head-support` merges `<head>`, `preload` for hover prefetch, `ws/sse` extensions for live regions without custom JS.
- Backend pairing: any stack — Express `if (req.headers['hx-request']) res.render('rows') else res.render('page')`; Django `template partials/`; Rails turbo-like partials; return `HX-Trigger: toast` response header for client events.
- Errors: `htmx:responseError` toast on 500, `HX-Retarget`/`HX-Reswap` response headers to override target/swap server-side; 422 validation → swap form with errors, 303 redirect honored.
- A11y/perf: focus moved to swapped heading (`autofocus` or `hx-swap` + JS), `aria-live` on `#toast`, fragment HTML <14kB, `preload` only above-fold links, no `every 1s` polling storms.
## Examples
- Live search: input `hx-get="/search" hx-trigger="keyup changed delay:300ms" hx-target="#results"` server returns `<li>` rows, `hx-indicator` spinner, `hx-push-url` preserves query.
- Todos CRUD: `hx-post` add appends `afterbegin`, row checkbox `hx-patch` swaps `outerHTML`, delete `hx-delete hx-swap="outerHTML swap:300ms"` + `hx-confirm`, OOB badge count.
- Infinite feed: sentinel `<div hx-get="/feed?page=2" hx-trigger="revealed" hx-swap="afterend">` each batch returns items + next sentinel, works with any backend pagination.
## Workflows
1. Boost: add `htmx.org@2` CDN + `<body hx-boost="true">`, `meta csrf`, `#toast[aria-live]` region, `.htmx-request` indicator CSS.
2. Slice UI: identify fragments (`#list`, `#form-errs`, `#badge`), create `partials/*.html` server templates returning fragments.
3. Wire: `hx-get/post` + `hx-target/swap/trigger`, `hx-push-url` on nav, `hx-confirm` on deletes, `hx-indicator` on slow calls.
4. Backend: branch on `HX-Request` (fragment vs page), 422 error HTML, `HX-Trigger` header for toasts, CSRF check.
5. Polish: OOB badge/toast swaps, `revealed` infinite scroll, `morph` where focus must persist, no-JS fallback verified by disabling JS.
## Templates
- Search + table with any backend:
```html
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<body hx-boost="true">
<input name="q" hx-get="/search" hx-trigger="keyup changed delay:300ms" hx-target="#results" hx-indicator="#spin" placeholder="Search…" />
<span id="spin" class="htmx-indicator">…</span>
<table><tbody id="results"><!-- server rows --></tbody></table>
<div id="toast" aria-live="polite"></div>
```
```js
// Express pairing
app.get('/search', (req, res) => {
  const rows = search(req.query.q).map((u) => `<tr><td>${u.name}</td><td>${u.email}</td></tr>`).join('');
  if (req.headers['hx-request']) return res.send(rows || '<tr><td>No matches</td></tr>');
  res.render('page', { rows });
});
```
- Create + OOB + delete:
```html
<form hx-post="/todos" hx-target="#list" hx-swap="afterbegin" hx-on::after-request="this.reset()">
  <input name="title" required /><button class="btn">Add</button>
</form>
<ul id="list"></ul>
<span id="badge">0</span>
<!-- server POST returns: <li id="t1">Buy milk <button hx-delete="/todos/t1" hx-target="closest li" hx-swap="outerHTML swap:200ms" hx-confirm="Delete?">x</button></li>
<span id="badge" hx-swap-oob="true">1</span><div id="toast" hx-swap-oob="true">Added!</div> -->
```
- Infinite scroll + polling:
```html
<div id="feed"><!-- batch 1 --></div>
<div hx-get="/feed?page=2" hx-trigger="revealed" hx-swap="afterend" hx-select="#feed > *"></div>
<div id="clock" hx-get="/clock" hx-trigger="every 30s" hx-swap="innerHTML">--:--</div>
<script>document.body.addEventListener('htmx:configRequest', (e) => {
  e.detail.headers['X-CSRFToken'] = document.querySelector('meta[name=csrf]').content; });</script>
```
## Tools
- SARAH shell_run for server dev, `curl -H "HX-Request: true"` fragment checks, Playwright no-JS fallback tests.
- SARAH file_write for partials/pages/endpoints; web_fetch for HTMX docs + extensions.
## Advanced
- Morph + head: `htmx-ext="head-support,morph"` keeps `<input>` focus + video playing across swaps; `hx-history="false"` for ephemeral widgets excluded from snapshots.
- SSE/WS live: `<div hx-ext="sse" sse-connect="/events" sse-swap="message">` ticker without JS timers; server `text/event-stream` with `HX-Trigger` payloads for toasts.
- Scale: fragment cache `Cache-Control: s-maxage=60` per `HX-Target`, `Vary: HX-Request` so CDN splits page vs fragment, `HX-Redirect` for auth expiry → login with `hx-push-url` intact.
## Documentation
- https://htmx.org/docs/
- https://htmx.org/attributes/hx-get/
- https://htmx.org/attributes/hx-swap/
- https://htmx.org/extensions/
- https://htmx.org/headers/hx-request/
