Back to skills
SKILL.md
Htmx Hypermedia
ASecurityExpert 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
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill htmx-hypermedia --agent claude-codeAre you the author of Htmx Hypermedia?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-htmx-hypermedia)---
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
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.