Claude Code for HTMX and Alpine.js: Hypermedia and Lightweight Interactivity — Claude Skills 360 Blog
Blog / Frontend / Claude Code for HTMX and Alpine.js: Hypermedia and Lightweight Interactivity
Frontend

Claude Code for HTMX and Alpine.js: Hypermedia and Lightweight Interactivity

Published: October 21, 2026
Read time: 8 min read
By: Claude Skills 360

HTMX and Alpine.js represent a different philosophy than SPA frameworks: server-rendered HTML with targeted dynamic updates, rather than full client-side rendering. HTMX makes any element issue HTTP requests and swap HTML fragments into the DOM. Alpine.js handles local component state without a build step. Together, they produce interactive UIs with dramatically less JavaScript — appropriate for teams wanting to keep the stack simple or incrementally enhance multi-page apps. Claude Code writes HTMX attribute patterns, Alpine.js components, and the server-side handlers that return HTML fragments.

CLAUDE.md for HTMX/Alpine Projects

## Frontend Stack
- HTMX 2.x for server-driven interactivity
- Alpine.js 3.x for client-side local state
- Server: Django, FastAPI, or Express returning HTML fragments (not JSON)
- Templating: Jinja2 (Django/FastAPI) or Handlebars (Node)
- CSS: Tailwind
- No build step: HTMX and Alpine loaded from CDN or vendored JS file
- Philosophy: server owns state, client enhances presentation
- Each HTMX endpoint returns an HTML fragment, not a full page

HTMX Fundamentals

<!-- Basic HTMX: GET on click, swap result into element -->
<button
  hx-get="/api/orders/status"
  hx-target="#order-status"
  hx-swap="innerHTML"
  hx-trigger="click"
>
  Refresh Status
</button>

<div id="order-status">Loading...</div>

<!-- POST form without page reload -->
<form
  hx-post="/api/orders/create"
  hx-target="#order-result"
  hx-swap="outerHTML"
  hx-indicator="#spinner"
>
  <input type="text" name="customer_id" required />
  <button type="submit">Place Order</button>
</form>

<!-- Loading indicator: shown during request -->
<div id="spinner" class="htmx-indicator">Processing...</div>

<!-- Infinite scroll: load next page when last item enters viewport -->
<div
  hx-get="/orders?page=2"
  hx-trigger="revealed"
  hx-swap="afterend"
  hx-target="this"
>
  <div class="order-item"><!-- last item --></div>
</div>

<!-- Live search with debounce -->
<input
  type="search"
  name="q"
  hx-get="/api/orders/search"
  hx-target="#results"
  hx-trigger="keyup changed delay:300ms"
  hx-indicator="#search-spinner"
  placeholder="Search orders..."
/>

Server-Side Fragment Handlers (FastAPI + Jinja2)

# routes/orders.py
from fastapi import APIRouter, Request, Depends, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse

router = APIRouter()
templates = Jinja2Templates(directory="templates")

@router.get("/api/orders/search", response_class=HTMLResponse)
async def search_orders(request: Request, q: str = ""):
    orders = await db.search_orders(q) if q else []
    
    # Return only the fragment — HTMX replaces #results with this
    return templates.TemplateResponse("fragments/order_list.html", {
        "request": request,
        "orders": orders,
        "query": q,
    })

@router.post("/api/orders/create", response_class=HTMLResponse)
async def create_order(
    request: Request,
    customer_id: str = Form(...),
    current_user: User = Depends(get_current_user),
):
    result = await place_order_service(customer_id, current_user)
    
    if result.success:
        # HX-Trigger: tell HTMX to trigger a custom event (for out-of-band updates)
        response = templates.TemplateResponse("fragments/order_created.html", {
            "request": request,
            "order": result.order,
        })
        response.headers["HX-Trigger"] = "orderCreated"
        return response
    else:
        # Return error fragment with 422 to replace the form
        response = templates.TemplateResponse("fragments/order_error.html", {
            "request": request,
            "error": result.error,
        })
        response.status_code = 422
        return response
<!-- templates/fragments/order_list.html -->
{% if orders %}
  <ul class="order-list">
    {% for order in orders %}
    <li class="order-item">
      <span class="order-id">#{{ order.id[-6:] }}</span>
      <span class="order-status badge badge-{{ order.status|lower }}">{{ order.status }}</span>
      <span class="order-total">${{ "%.2f"|format(order.total_cents / 100) }}</span>
    </li>
    {% endfor %}
  </ul>
{% else %}
  <p class="empty-state">No orders found{% if query %} for "{{ query }}"{% endif %}.</p>
{% endif %}

Out-of-Band Swaps

<!-- Update multiple parts of the page from a single response -->
<!-- The server response can include hx-swap-oob elements -->

<!-- Main swap target: #order-form -->
<div id="new-order-form">
  <!-- Replaced with success message -->
  <p>Order placed successfully!</p>
</div>

<!-- Out-of-band update: sidebar counter and notification badge both update automatically -->
<span id="order-count" hx-swap-oob="true">42</span>
<span id="notification-badge" hx-swap-oob="true" class="badge">1</span>
# Server returns both the main fragment AND out-of-band updates
@router.post("/api/orders/create", response_class=HTMLResponse)
async def create_order(request: Request, ...):
    order = await place_order(...)
    new_count = await db.count_orders()
    
    # Render main content + OOB updates in one response
    main = render_template("fragments/order_success.html", order=order)
    oob_count = f'<span id="order-count" hx-swap-oob="true">{new_count}</span>'
    
    return HTMLResponse(main + oob_count)

Server-Sent Events for Real-Time

<!-- HTMX SSE extension: live order status updates -->
<div
  hx-ext="sse"
  sse-connect="/api/orders/123/events"
  sse-swap="order-status-update"
  hx-target="#order-status-content"
  hx-swap="innerHTML"
>
  <div id="order-status-content">
    <p>Status: Pending</p>
  </div>
</div>
from fastapi.responses import StreamingResponse
import asyncio

@router.get("/api/orders/{order_id}/events")
async def order_events(order_id: str):
    async def event_generator():
        previous_status = None
        while True:
            order = await db.get_order(order_id)
            
            if order.status != previous_status:
                previous_status = order.status
                html_fragment = render_template("fragments/order_status.html", order=order)
                
                # SSE format: event name must match sse-swap attribute
                yield f"event: order-status-update\n"
                yield f"data: {html_fragment}\n\n"
                
                if order.status in ("DELIVERED", "CANCELLED"):
                    break  # Stop streaming when terminal state reached
            
            await asyncio.sleep(2)
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )

Alpine.js for Local State

<!-- Alpine.js: client-side state without server round-trips -->
<!-- Cart quantity selector -->
<div x-data="{ quantity: 1 }">
  <button @click="quantity = Math.max(1, quantity - 1)">−</button>
  <input
    type="number"
    x-model="quantity"
    min="1"
    max="99"
    class="quantity-input"
  />
  <button @click="quantity = Math.min(99, quantity + 1)">+</button>
  
  <!-- Display computed value -->
  <span x-text="`Total: $${(quantity * 19.99).toFixed(2)}`"></span>
  
  <!-- Pass to HTMX POST via hidden input -->
  <input type="hidden" name="quantity" :value="quantity" />
</div>

<!-- Dropdown with click-outside-to-close -->
<div
  x-data="{ open: false }"
  @keydown.escape.window="open = false"
  @click.outside="open = false"
  class="dropdown"
>
  <button @click="open = !open" :aria-expanded="open">
    Filter <span x-text="open ? '▲' : '▼'"></span>
  </button>
  
  <div x-show="open" x-transition class="dropdown-menu">
    <a href="?status=PENDING">Pending</a>
    <a href="?status=SHIPPED">Shipped</a>
  </div>
</div>

<!-- Form validation with Alpine -->
<form
  x-data="{
    email: '',
    get isValidEmail() { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email) },
    submitted: false
  }"
  hx-post="/api/newsletter/subscribe"
  @submit="submitted = true"
>
  <input
    type="email"
    x-model="email"
    :class="{ 'border-red-500': submitted && !isValidEmail }"
    name="email"
    placeholder="[email protected]"
  />
  <span x-show="submitted && !isValidEmail" class="text-red-500">
    Please enter a valid email
  </span>
  <button type="submit" :disabled="submitted && !isValidEmail">Subscribe</button>
</form>

HX-Boost: Progressive Enhancement

<!-- hx-boost: turns regular links and forms into HTMX-enhanced requests
     The whole page still works without JavaScript -->
<body hx-boost="true">
  <!-- These links now load without full page reload -->
  <nav>
    <a href="/orders">Orders</a>
    <a href="/products">Products</a>
    <a href="/settings">Settings</a>
  </nav>
  
  <!-- Title updates automatically from <title> in response -->
  <!-- Browser history updates via pushState -->
</body>

For the Django templates that HTMX fragments integrate with, see the Django REST Framework guide for backend patterns. For comparing with full React SPAs, the React guide covers when a SPA is worth the complexity. The Claude Skills 360 bundle includes HTMX/Alpine skill sets covering fragment patterns, SSE, and out-of-band updates. Start with the free tier to try HTMX endpoint generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free