Expert in HTMX, hypermedia-driven applications, and modern server-rendered UI patterns. Use when you need help with htmx hypermedia.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill htmx-hypermedia --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Htmx Hypermedia?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-htmx-hypermedia)More formats (shields.io, HTML) on the badges page.
---
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/)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!