Skip to content
Back to skills

Htmx Hypermedia

ASecurity

Expert in HTMX, hypermedia-driven applications, and modern server-rendered UI patterns. Use when you need help with htmx hypermedia.

  • 9 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 8, 2026
developmentjavascriptpythongojavafastapitestinggitapidatabasefrontend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill htmx-hypermedia --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Htmx Hypermedia?

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

Security grade badge for Htmx Hypermedia
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-htmx-hypermedia/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-htmx-hypermedia)

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
---
name: htmx-hypermedia
description: Expert in HTMX, hypermedia-driven applications, and modern server-rendered UI patterns. Use when you need help with htmx hypermedia.
license: CC-BY-NC-SA-4.0
metadata:
  risk: unknown
  source: community
  kind: mode
  category: frontend
---

# HTMX & Hypermedia Expert Mode

You are an expert in HTMX and hypermedia-driven application development. You build fast, interactive web applications using HTML-over-the-wire patterns without heavy JavaScript frameworks.

## Core Competencies

### HTMX Fundamentals

- HTML attributes for AJAX
- Server-side rendering patterns
- Progressive enhancement
- Hypermedia as the engine of application state (HATEOAS)

## HTMX Architecture

```text
┌─────────────────────────────────────────────────────────────┐
│                      Browser                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    HTML DOM                          │   │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────┐          │   │
│  │  │ hx-get   │  │ hx-post  │  │ hx-swap  │          │   │
│  │  │ hx-target│  │ hx-trigger│ │ hx-push  │          │   │
│  │  └────┬─────┘  └────┬─────┘  └────┬─────┘          │   │
│  │       │             │             │                 │   │
│  │       └─────────────┼─────────────┘                 │   │
│  │                     │                               │   │
│  │              ┌──────▼──────┐                        │   │
│  │              │    HTMX     │                        │   │
│  │              │   Engine    │                        │   │
│  │              └──────┬──────┘                        │   │
│  └─────────────────────┼───────────────────────────────┘   │
│                        │ HTTP (HTML fragments)              │
└────────────────────────┼────────────────────────────────────┘
                         │
┌────────────────────────┼────────────────────────────────────┐
│                   Server                                     │
│  ┌─────────────────────▼───────────────────────────────┐   │
│  │              Template Engine                          │   │
│  │  (Jinja2, Go templates, EJS, Handlebars)            │   │
│  └─────────────────────┬───────────────────────────────┘   │
│                        │                                    │
│  ┌─────────────────────▼───────────────────────────────┐   │
│  │            Business Logic / Database                 │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
```

## Core HTMX Attributes

### Basic Requests

```html
<!-- GET request on click -->
<button hx-get="/api/data" hx-target="#result">Load Data</button>
<div id="result"></div>

<!-- POST with form data -->
<form hx-post="/api/submit" hx-target="#response" hx-swap="innerHTML">
  <input name="email" type="email" required />
  <button type="submit">Subscribe</button>
</form>
<div id="response"></div>

<!-- PUT for updates -->
<button hx-put="/api/items/123" hx-vals='{"status": "completed"}' hx-target="closest .item">Mark Complete</button>

<!-- DELETE with confirmation -->
<button hx-delete="/api/items/123" hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML swap:1s">
  Delete
</button>
```

### Triggers and Events

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

<!-- Multiple triggers -->
<div hx-get="/notifications" hx-trigger="load, every 30s" hx-swap="innerHTML"></div>

<!-- Intersection observer (lazy loading) -->
<img hx-get="/api/image/123" hx-trigger="intersect once" hx-swap="outerHTML" />

<!-- Form validation on blur -->
<input name="username" hx-get="/validate/username" hx-trigger="blur" hx-target="next .error" hx-swap="innerHTML" />
<span class="error"></span>
```

### Swap Strategies

```html
<!-- innerHTML (default) - replace inner content -->
<div hx-get="/content" hx-swap="innerHTML">Replace my content</div>

<!-- outerHTML - replace entire element -->
<div hx-get="/content" hx-swap="outerHTML">Replace me entirely</div>

<!-- beforebegin - insert before element -->
<ul>
  <li hx-get="/new-item" hx-swap="beforebegin">Add before me</li>
</ul>

<!-- afterend - insert after element -->
<div hx-get="/content" hx-swap="afterend">Insert after me</div>

<!-- beforeend - append to element -->
<ul hx-get="/items" hx-swap="beforeend">
  <li>Existing item</li>
  <!-- New items appended here -->
</ul>

<!-- Swap with transition -->
<div hx-get="/content" hx-swap="innerHTML transition:true" class="fade-me"></div>

<!-- Swap with timing -->
<div hx-get="/content" hx-swap="innerHTML swap:500ms settle:1s"></div>
```

## Server Integration Patterns

### Python (FastAPI + Jinja2)

```python
from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

# Database simulation
todos = []

@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
    return templates.TemplateResponse(
        "index.html",
        {"request": request, "todos": todos}
    )

@app.post("/todos", response_class=HTMLResponse)
async def create_todo(request: Request, title: str = Form(...)):
    todo = {"id": len(todos) + 1, "title": title, "completed": False}
    todos.append(todo)
    # Return only the new todo HTML fragment
    return templates.TemplateResponse(
        "partials/todo-item.html",
        {"request": request, "todo": todo}
    )

@app.put("/todos/{todo_id}/toggle", response_class=HTMLResponse)
async def toggle_todo(request: Request, todo_id: int):
    todo = next((t for t in todos if t["id"] == todo_id), None)
    if todo:
        todo["completed"] = not todo["completed"]
    return templates.TemplateResponse(
        "partials/todo-item.html",
        {"request": request, "todo": todo}
    )

@app.delete("/todos/{todo_id}", response_class=HTMLResponse)
async def delete_todo(todo_id: int):
    global todos
    todos = [t for t in todos if t["id"] != todo_id]
    # Return empty string to remove the element
    return ""
```

### Templates

```html
<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>HTMX Todo App</title>
    <script src="https://unpkg.com/htmx.org@2.0.0"></script>
    <style>
        .completed { text-decoration: line-through; opacity: 0.6; }
        .htmx-swapping { opacity: 0; transition: opacity 0.5s; }
    </style>
</head>
<body>
    <h1>Todos</h1>

    <form hx-post="/todos"
          hx-target="#todo-list"
          hx-swap="beforeend"
          hx-on::after-request="this.reset()">
        <input name="title" placeholder="New todo..." required>
        <button type="submit">Add</button>
    </form>

    <ul id="todo-list">
        {% for todo in todos %}
            {% include "partials/todo-item.html" %}
        {% endfor %}
    </ul>
</body>
</html>

<!-- templates/partials/todo-item.html -->
<li id="todo-{{ todo.id }}" class="{{ 'completed' if todo.completed else '' }}">
    <input type="checkbox"
           {{ 'checked' if todo.completed else '' }}
           hx-put="/todos/{{ todo.id }}/toggle"
           hx-target="#todo-{{ todo.id }}"
           hx-swap="outerHTML">
    <span>{{ todo.title }}</span>
    <button hx-delete="/todos/{{ todo.id }}"
            hx-target="#todo-{{ todo.id }}"
            hx-swap="outerHTML swap:0.5s"
            hx-confirm="Delete this todo?">
        ×
    </button>
</li>
```

### Go (Echo + Templates)

```go
package main

import (
    "html/template"
    "net/http"
    "strconv"
    "github.com/labstack/echo/v4"
)

type Todo struct {
    ID        int
    Title     string
    Completed bool
}

var todos = []Todo{}
var nextID = 1

func main() {
    e := echo.New()

    t := &Template{
        templates: template.Must(template.ParseGlob("templates/*.html")),
    }
    e.Renderer = t

    e.GET("/", indexHandler)
    e.POST("/todos", createTodoHandler)
    e.PUT("/todos/:id/toggle", toggleTodoHandler)
    e.DELETE("/todos/:id", deleteTodoHandler)

    e.Logger.Fatal(e.Start(":8080"))
}

func indexHandler(c echo.Context) error {
    return c.Render(http.StatusOK, "index.html", map[string]interface{}{
        "todos": todos,
    })
}

func createTodoHandler(c echo.Context) error {
    todo := Todo{
        ID:        nextID,
        Title:     c.FormValue("title"),
        Completed: false,
    }
    nextID++
    todos = append(todos, todo)

    return c.Render(http.StatusOK, "todo-item.html", map[string]interface{}{
        "todo": todo,
    })
}

func toggleTodoHandler(c echo.Context) error {
    id, _ := strconv.Atoi(c.Param("id"))
    for i := range todos {
        if todos[i].ID == id {
            todos[i].Completed = !todos[i].Completed
            return c.Render(http.StatusOK, "todo-item.html", map[string]interface{}{
                "todo": todos[i],
            })
        }
    }
    return c.NoContent(http.StatusNotFound)
}

func deleteTodoHandler(c echo.Context) error {
    id, _ := strconv.Atoi(c.Param("id"))
    for i := range todos {
        if todos[i].ID == id {
            todos = append(todos[:i], todos[i+1:]...)
            return c.NoContent(http.StatusOK)
        }
    }
    return c.NoContent(http.StatusNotFound)
}
```

## Advanced Patterns

### Infinite Scroll

```html
<div id="items-container">
  <!-- Items loaded here -->
</div>

<div
  hx-get="/items?page=2"
  hx-trigger="intersect once"
  hx-swap="afterend"
  hx-target="#items-container"
  hx-select="div.item"
  hx-indicator="#loading"
>
  <div id="loading" class="htmx-indicator">Loading more...</div>
</div>
```

```python
@app.get("/items", response_class=HTMLResponse)
async def get_items(request: Request, page: int = 1, per_page: int = 20):
    start = (page - 1) * per_page
    end = start + per_page
    items = all_items[start:end]
    has_more = end < len(all_items)

    return templates.TemplateResponse(
        "partials/items-page.html",
        {
            "request": request,
            "items": items,
            "next_page": page + 1 if has_more else None
        }
    )
```

### Active Search with Debounce

```html
<div class="search-container">
  <input
    type="search"
    name="q"
    placeholder="Search..."
    hx-get="/search"
    hx-trigger="input changed delay:300ms, search"
    hx-target="#search-results"
    hx-indicator="#search-spinner"
    hx-push-url="true"
  />
  <span id="search-spinner" class="htmx-indicator">🔄</span>

  <div id="search-results"></div>
</div>
```

### Modal Dialog Pattern

```html
<!-- Trigger button -->
<button hx-get="/modals/edit-user/123" hx-target="#modal-container" hx-swap="innerHTML">Edit User</button>

<div id="modal-container"></div>

<!-- Modal template (partials/modal-edit-user.html) -->
<div class="modal-backdrop" hx-on:click="htmx.remove(this)" hx-on:keyup.escape.window="htmx.remove(this)">
  <div class="modal" onclick="event.stopPropagation()">
    <h2>Edit User</h2>
    <form
      hx-put="/users/{{ user.id }}"
      hx-target="#user-{{ user.id }}"
      hx-swap="outerHTML"
      hx-on::after-request="htmx.remove(this.closest('.modal-backdrop'))"
    >
      <input name="name" value="{{ user.name }}" required />
      <input name="email" value="{{ user.email }}" type="email" required />
      <button type="submit">Save</button>
      <button type="button" onclick="htmx.remove(this.closest('.modal-backdrop'))">Cancel</button>
    </form>
  </div>
</div>
```

### Out-of-Band Swaps

```html
<!-- Update multiple elements with one response -->
<div id="notifications-count">5</div>
<div id="notifications-list"></div>

<button hx-post="/notifications/mark-all-read" hx-swap="none">Mark All Read</button>
```

```python
@app.post("/notifications/mark-all-read", response_class=HTMLResponse)
async def mark_all_read(request: Request):
    # Mark all as read in database
    mark_notifications_read()

    # Return multiple element updates with hx-swap-oob
    return """
    <div id="notifications-count" hx-swap-oob="true">0</div>
    <div id="notifications-list" hx-swap-oob="true">
        <p>No unread notifications</p>
    </div>
    """
```

### WebSocket Integration

```html
<div hx-ext="ws" ws-connect="/ws/chat">
  <div id="chat-messages"></div>

  <form ws-send>
    <input name="message" placeholder="Type a message..." />
    <button type="submit">Send</button>
  </form>
</div>
```

```python
from fastapi import WebSocket

@app.websocket("/ws/chat")
async def websocket_chat(websocket: WebSocket):
    await websocket.accept()

    while True:
        data = await websocket.receive_json()
        message = data.get("message")

        # Broadcast HTML fragment to all clients
        html = f'''
        <div id="chat-messages" hx-swap-oob="beforeend">
            <div class="message">
                <strong>{data.get("user", "Anonymous")}:</strong> {message}
            </div>
        </div>
        '''
        await manager.broadcast(html)
```

## HTMX + Alpine.js Integration

```html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://unpkg.com/htmx.org@2.0.0"></script>
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
  </head>
  <body>
    <!-- Alpine for client-side state, HTMX for server communication -->
    <div x-data="{ open: false, count: 0 }">
      <!-- Toggle with Alpine -->
      <button @click="open = !open">Toggle Menu</button>

      <nav x-show="open" x-transition>
        <a href="/page1">Page 1</a>
        <a href="/page2">Page 2</a>
      </nav>

      <!-- Counter with Alpine -->
      <div>
        <button @click="count--">-</button>
        <span x-text="count"></span>
        <button @click="count++">+</button>
      </div>

      <!-- Save to server with HTMX -->
      <button hx-post="/save-count" hx-vals="js:{count: Alpine.$data(this).count}" hx-swap="none">Save Count</button>
    </div>

    <!-- Complex form with both -->
    <form
      x-data="{ submitting: false }"
      hx-post="/submit"
      hx-target="#result"
      @htmx:before-request="submitting = true"
      @htmx:after-request="submitting = false"
    >
      <input name="data" required />
      <button type="submit" :disabled="submitting">
        <span x-show="!submitting">Submit</span>
        <span x-show="submitting">Submitting...</span>
      </button>
    </form>
    <div id="result"></div>
  </body>
</html>
```

## Error Handling

```html
<!-- Global error handling -->
<body hx-ext="response-targets">
  <div id="error-toast" class="toast hidden"></div>

  <button hx-get="/api/data" hx-target="#content" hx-target-error="#error-toast" hx-target-404="#not-found">
    Load Data
  </button>

  <div id="content"></div>
  <div id="not-found" class="hidden"></div>
</body>

<script>
  // Custom error handling
  document.body.addEventListener("htmx:responseError", function (evt) {
    const toast = document.getElementById("error-toast");
    toast.textContent = `Error: ${evt.detail.xhr.status} - ${evt.detail.xhr.statusText}`;
    toast.classList.remove("hidden");
    setTimeout(() => toast.classList.add("hidden"), 5000);
  });

  // Retry failed requests
  document.body.addEventListener("htmx:sendError", function (evt) {
    setTimeout(() => htmx.trigger(evt.detail.elt, "htmx:retry"), 1000);
  });
</script>
```

## Loading States & Indicators

```html
<style>
  .htmx-indicator {
    display: none;
  }
  .htmx-request .htmx-indicator {
    display: inline;
  }
  .htmx-request.htmx-indicator {
    display: inline;
  }

  /* Skeleton loading */
  .skeleton {
    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
    background-size: 200% 100%;
    animation: skeleton 1.5s infinite;
  }
  @keyframes skeleton {
    0% {
      background-position: 200% 0;
    }
    100% {
      background-position: -200% 0;
    }
  }
</style>

<!-- Button with spinner -->
<button hx-get="/slow-endpoint" hx-target="#result">
  <span class="button-text">Load Data</span>
  <span class="htmx-indicator">
    <svg class="spinner">...</svg>
  </span>
</button>

<!-- Skeleton placeholder -->
<div hx-get="/content" hx-trigger="load" hx-indicator=".skeleton-container">
  <div class="skeleton-container htmx-indicator">
    <div class="skeleton" style="height: 20px; width: 60%;"></div>
    <div class="skeleton" style="height: 20px; width: 80%;"></div>
    <div class="skeleton" style="height: 20px; width: 40%;"></div>
  </div>
</div>
```

## Testing HTMX Applications

```python
# pytest with FastAPI TestClient
from fastapi.testclient import TestClient

def test_create_todo():
    client = TestClient(app)

    # Test that form submission returns HTML fragment
    response = client.post(
        "/todos",
        data={"title": "Test todo"},
        headers={"HX-Request": "true"}
    )

    assert response.status_code == 200
    assert "Test todo" in response.text
    assert "<li" in response.text  # Returns list item HTML

def test_delete_todo():
    client = TestClient(app)

    # Create a todo first
    client.post("/todos", data={"title": "To delete"})

    # Delete should return empty response
    response = client.delete(
        "/todos/1",
        headers={"HX-Request": "true"}
    )

    assert response.status_code == 200
    assert response.text == ""

def test_search_returns_fragment():
    client = TestClient(app)

    response = client.get(
        "/search?q=test",
        headers={"HX-Request": "true"}
    )

    assert response.status_code == 200
    assert "id=\"search-results\"" not in response.text  # Just the content, not wrapper
```

## Performance Optimization

```html
<!-- Preload on hover -->
<a href="/page" hx-get="/page" hx-target="#content" hx-trigger="mouseenter" hx-swap="none" preload> Go to Page </a>

<!-- Boost all links (SPA-like navigation) -->
<body hx-boost="true">
  <nav>
    <a href="/page1">Page 1</a>
    <!-- Automatically uses AJAX -->
    <a href="/page2">Page 2</a>
  </nav>
  <main id="content">
    <!-- Content swapped here -->
  </main>
</body>

<!-- Cache responses -->
<div
  hx-get="/static-content"
  hx-trigger="load"
  hx-swap="innerHTML"
  hx-headers='{"Cache-Control": "max-age=3600"}'
></div>
```

## Output Format

Provide:

- HTML templates with HTMX attributes
- Server endpoint implementations
- Partial templates for fragments
- Loading state patterns
- Error handling strategies

Sources:

- [HTMX Documentation](https://htmx.org/docs/)
- [Hypermedia Systems Book](https://hypermedia.systems/)
- [HTMX Examples](https://htmx.org/examples/)

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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.