Skip to content
Back to skills

Htmx Progressive Enhancement

ASecurity

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, ac...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 24, 2026
developmentpythongobashreactvuenodedjangoflaskrailsapi

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add curiositech/port-daddy --skill htmx-progressive-enhancement --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Htmx Progressive Enhancement?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Htmx Progressive Enhancement
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/curiositech-htmx-progressive-enhancement-port-daddy/badge)](https://www.skillsdirectory.com/skills/curiositech-htmx-progressive-enhancement-port-daddy)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
license: Apache-2.0
name: htmx-progressive-enhancement
allowed-tools: Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch
description: '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:
      - kind: ui-requirement
        format: markdown
        description: The interaction to build -- partial updates, form flows, live regions, SPA-migration scope -- from a human or another agent.
      - kind: htmx-ui-plan
        format: json
        description: A structured plan of response format, CSRF handling, no-JS fallback, a11y, and swap strategy, matching schemas/htmx-progressive-enhancement-plan.schema.json.
    produces:
      - kind: enhancement-design
        format: markdown
        description: The hx-* attribute composition, server fragment contract, and accessibility wiring, following this skill's patterns.
      - kind: htmx-audit-report
        format: json
        description: A deterministic pass/fail audit of the htmx-ui-plan against this skill's Quality Gates, as produced by scripts/htmx_progressive_enhancement_audit.mjs.
---

# 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.

## Core capabilities

### Basic request/swap

```html
<button hx-get="/api/widget" hx-target="#out" hx-swap="innerHTML">
  Load
</button>
<div id="out"></div>
```

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

```html
<!-- Replace contents of target -->
<a hx-get="/feed/items" hx-target="#list" hx-swap="innerHTML">Refresh</a>

<!-- Replace the target itself -->
<button hx-delete="/items/42" hx-target="closest tr" hx-swap="outerHTML swap:300ms">
  Delete
</button>

<!-- Append to a list (newest at top) -->
<form hx-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.

### Triggers — debouncing search

```html
<input
  type="search"
  name="q"
  hx-get="/search"
  hx-trigger="keyup changed delay:300ms, search"
  hx-target="#results"
/>
<div id="results"></div>
```

`changed` skips events that don't change the value (arrow keys). `delay:300ms` debounces. Multiple comma-separated triggers are OR'd.

### Out-of-band swaps

When a request needs to update something outside the target:

```html
<!-- Server returns -->
<div id="cart-count" hx-swap-oob="true">3</div>
<div>Item added to cart.</div>
```

The browser sees the OOB element and swaps it into the page wherever `id="cart-count"` lives. The remaining content goes to `hx-target`.

### Response headers — server controls the client

| Header | Effect |
|--------|--------|
| `HX-Trigger` | Fire a custom event on the document. JSON for events with payload. |
| `HX-Push-Url` | Update browser URL. |
| `HX-Redirect` | Client-side redirect. |
| `HX-Refresh: true` | Reload the page. |
| `HX-Reswap` / `HX-Retarget` | Override the swap strategy or target server-side. |
| `HX-Location` | Like Redirect, but uses htmx (preserves boost). |

```python
# Flask example
return render_template('partials/item.html', item=item), 200, {
    'HX-Trigger': json.dumps({'cart-updated': {'count': 3}})
}
```

```html
<div hx-get="/cart/count" hx-trigger="cart-updated from:body">...</div>
```

### Forms with validation

```html
<form hx-post="/users" hx-target="#form-area" hx-swap="outerHTML">
  <label>
    Email
    <input name="email" required hx-post="/validate/email"
           hx-trigger="change" hx-target="next .error" hx-swap="innerHTML" />
    <span class="error"></span>
  </label>
  <button>Save</button>
</form>
```

The change-time validation hits `/validate/email`; the form-submit hits `/users`. Server returns either the next form state or the success view.

### Boost — turn regular links into AJAX

```html
<body hx-boost="true">
  <a href="/about">About</a>      <!-- becomes an htmx swap -->
  <a href="/external" hx-boost="false">External</a>
</body>
```

Boosted navigation does an AJAX GET, swaps `<body>`, and pushes URL. SPA-like navigation without writing a SPA.

### Loading indicators

```html
<button hx-post="/submit" hx-indicator="#spinner">Submit</button>
<span id="spinner" class="htmx-indicator">Saving…</span>
```

```css
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline; }
```

The `htmx-request` class is added to the element making the request for the duration; CSS handles the rest.

### Server-sent events

```html
<div hx-ext="sse" sse-connect="/events" sse-swap="message">
  Waiting for events…
</div>
```

Each `event: message` from the server triggers a swap of the response body into the div.

### WebSockets

```html
<div hx-ext="ws" ws-connect="/chat">
  <ul id="messages"></ul>
  <form ws-send>
    <input name="text" />
  </form>
</div>
```

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:

```bash
node scripts/htmx_progressive_enhancement_audit.mjs --input examples/sample-input.json
```

`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.

Files in this skill

  • CHANGELOG.md700 B
  • SKILL.md11.5 KB
  • examples/sample-input.json339 B
  • schemas/htmx-progressive-enhancement-plan.schema.json2.5 KB
  • scripts/htmx_progressive_enhancement_audit.mjs7.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…