htmx Expert

Expert guidance for htmx development, including AJAX interactions, htmx attributes (hx-get, hx-post, hx-swap, hx-target, hx-trigger), debugging htmx behavior, building hypermedia-driven applications, and htmx best practices.

---
name: htmx-expert
description: "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)"
---

# htmx Expert

This skill provides comprehensive guidance for htmx development, the library that extends HTML to access modern browser features directly without JavaScript.

## Core Philosophy

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.

## When to Use This Skill

- Implementing htmx attributes and interactions
- Building hypermedia-driven applications
- Debugging htmx request/response cycles
- Converting SPA patterns to htmx approaches
- Understanding htmx events and lifecycle
- Configuring htmx extensions
- Implementing proper security measures

## Core Attributes Reference

### HTTP Verb Attributes

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

### Request Control

- **hx-trigger**: Customize when requests fire
  - Modifiers: `changed`, `delay:Xms`, `throttle:Xms`, `once`
  - Special triggers: `load`, `revealed`, `every Xs`
  - Extended: `from:<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)

### Response Handling

- **hx-target**: Where to place response content
  - Extended selectors: `this`, `closest <sel>`, `next <sel>`, `previous <sel>`, `find <sel>`

- **hx-swap**: How to insert content
  - `innerHTML` (default), `outerHTML`, `beforebegin`, `afterbegin`, `beforeend`, `afterend`, `delete`, `none`
  - Modifiers: `swap:Xms`, `settle:Xms`, `scroll:top`, `show:top`

- **hx-select**: Select subset of response to swap
- **hx-select-oob**: Select elements for out-of-band swaps

### State Management

- **hx-push-url**: Push URL to browser history
- **hx-replace-url**: Replace current URL in history
- **hx-history**: Control history snapshot behavior
- **hx-history-elt**: Specify element to snapshot

### UI Indicators

- **hx-indicator**: Element to show during request (add `htmx-indicator` class)
- **hx-disabled-elt**: Elements to disable during request

### Security & Control

- **hx-confirm**: Show confirmation dialog before request
- **hx-validate**: Enable HTML5 validation on non-form elements
- **hx-disable**: Disable htmx processing on element and descendants
- **hx-sync**: Coordinate requests between elements

## Implementation Patterns

### Basic AJAX Pattern

```html
<button hx-get="/api/data"
        hx-target="#result"
        hx-swap="innerHTML">
  Load Data
</button>
<div id="result"></div>
```

### Active Search

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

### Infinite Scroll

```html
<div hx-get="/items?page=2"
     hx-trigger="revealed"
     hx-swap="afterend">
  Loading more...
</div>
```

### Polling

```html
<div hx-get="/status"
     hx-trigger="every 5s"
     hx-swap="innerHTML">
  Status: Unknown
</div>
```

### Form Submission

```html
<form hx-post="/submit"
      hx-target="#response"
      hx-swap="outerHTML">
  <input name="email" type="email" required>
  <button type="submit">Submit</button>
</form>
```

### Out-of-Band Updates

Server response can update multiple elements:

```html
<!-- 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>
```

### Loading Indicators

```html
<button hx-get="/slow-endpoint"
        hx-indicator="#spinner">
  Load
</button>
<img id="spinner" class="htmx-indicator" src="/spinner.gif">
```

CSS for indicators:
```css
.htmx-indicator {
  opacity: 0;
  transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator {
  opacity: 1;
}
```

## Server Response Patterns

### Return HTML Fragments

Server endpoints return HTML, not JSON:

```python
# Flask example
@app.route('/search')
def search():
    q = request.args.get('q', '')
    results = search_database(q)
    return render_template('_search_results.html', results=results)
```

### Response Headers

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 |

### Detect htmx Requests

Check `HX-Request` header to differentiate htmx from regular requests:

```python
if request.headers.get('HX-Request'):
    return render_template('_partial.html')
else:
    return render_template('full_page.html')
```

## Events

### Key Events

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

### Event Handling

Using `hx-on*`:
```html
<button hx-get="/data"
        hx-on:htmx:before-request="console.log('Starting...')"
        hx-on:htmx:after-swap="console.log('Done!')">
  Load
</button>
```

Using JavaScript:
```javascript
document.body.addEventListener('htmx:configRequest', function(evt) {
  evt.detail.headers['X-Custom-Header'] = 'value';
});
```

## Security Best Practices

1. **Escape All User Content**: Prevent XSS through server-side template escaping
2. **Use hx-disable**: Prevent htmx processing on untrusted content
3. **Restrict Request Origins**:
   ```javascript
   htmx.config.selfRequestsOnly = true;
   ```
4. **Disable Script Processing**:
   ```javascript
   htmx.config.allowScriptTags = false;
   ```
5. **Include CSRF Tokens**:
   ```html
   <body hx-headers='{"X-CSRF-Token": ""}'>
   ```
6. **Content Security Policy**: Layer browser-level protections

## Configuration

Key `htmx.config` options:

```javascript
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:
```html
<meta name="htmx-config" content='{"selfRequestsOnly":true}'>
```

## Extensions

### Loading Extensions

```html
<script src="https://unpkg.com/htmx-ext-<name>@<version>/<name>.js"></script>
<body hx-ext="extension-name">
```

### Common Extensions

- **head-support**: Merge head tag information across requests
- **idiomorph**: Morphing swaps (preserves element state)
- **sse**: Server-Sent Events support
- **ws**: WebSocket support
- **preload**: Content preloading
- **response-targets**: HTTP status-based targeting

## Debugging

Enable logging:
```javascript
htmx.logAll();
```

Check request headers in Network tab:
- `HX-Request: true`
- `HX-Target: <target-id>`
- `HX-Trigger: <trigger-id>`
- `HX-Current-URL: <page-url>`

## Progressive Enhancement

Structure for graceful degradation:

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

## Third-Party Integration

Initialize libraries on htmx-loaded content:

```javascript
htmx.onLoad(function(content) {
  content.querySelectorAll('.datepicker').forEach(el => {
    new Datepicker(el);
  });
});
```

For programmatically added htmx content:
```javascript
htmx.process(document.getElementById('new-content'));
```

## Common Gotchas

1. **ID Stability**: Keep element IDs stable for CSS transitions
2. **Swap Timing**: Default 0ms swap delay; use `swap:100ms` for transitions
3. **Event Bubbling**: htmx events bubble; use `event.detail` for data
4. **Form Data**: Only named inputs are included in requests
5. **History**: History snapshots store innerHTML, not full DOM state

## Development Environment Requirements

### htmx Requires HTTP (Not file://)

htmx will NOT work when opening HTML files directly from the filesystem (`file://` URLs). This causes `htmx:invalidPath` errors because:
- Browsers block cross-origin requests from `file://` URLs
- htmx needs to make HTTP requests to endpoints

**Solution**: Always serve htmx applications via HTTP server:

```bash
# Simple Python server (recommended for development)
python3 -m http.server 8000

# Or create a custom server with API endpoints
python3 server.py
```

### Minimal Development Server Pattern

For htmx examples and prototypes, create a simple Python server that:
1. Serves static files (HTML, CSS, JS)
2. Provides API endpoints that return HTML fragments

```python
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()
```

## Practical Implementation Lessons

### Loading Indicators with CSS Spinner

Use CSS-only spinners instead of image files for better performance:

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

### Input Search with Proper Trigger

Use `input changed` instead of `keyup changed` for better UX (catches paste, autofill):

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

### Self-Targeting with Polling

For elements that replace themselves (polling), use `hx-target="this"`:

```html
<div hx-get="/api/time"
     hx-trigger="load, every 2s"
     hx-target="this"
     hx-swap="innerHTML">
    Loading...
</div>
```

### Row Updates with closest

For list items where each row has its own update button:

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

### Event Attribute Syntax

The `hx-on::` syntax uses double colons for htmx events:

```html
<!-- Correct -->
<button hx-on::before-request="console.log('starting')">

<!-- Also correct (older syntax) -->
<button hx-on:htmx:before-request="console.log('starting')">
```

### Combining Multiple Triggers

Separate triggers with commas:

```html
<div hx-get="/api/data"
     hx-trigger="load, every 5s, click from:#refresh-btn">
```

### Form POST with Loading State

Combine `hx-indicator` and `hx-disabled-elt` for complete UX:

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

## Additional Resources

For detailed reference, consult:
- Official docs: https://htmx.org/docs/
- Attributes reference: https://htmx.org/reference/
- Examples: https://htmx.org/examples/