HTMX: Build Dynamic Web Apps Without Writing JavaScript
Learn how HTMX enables dynamic web interactions using HTML attributes alone. No JavaScript frameworks needed — just HTML, CSS, and a backend.
// table of contents (22 sections)
The JavaScript fatigue is real. What if you could build dynamic, interactive web applications without managing complex state, learning new frameworks every year, or shipping megabytes of client-side code?
HTMX offers exactly that — a return to the original vision of the web as a hypermedia system. No React. No Vue. No build steps. Just HTML attributes that tell the browser: “When this happens, fetch HTML from the server and put it here.”
What is HTMX?
HTMX is a small (~14KB minified) JavaScript library that extends HTML with special attributes. These attributes let you trigger AJAX requests, CSS transitions, and WebSocket connections directly from HTML elements — no JavaScript required.
<button hx-get="/users" hx-target="#user-list">
Load Users
</button>
<div id="user-list"></div>
When clicked, this button:
- Sends a GET request to
/users - Receives HTML fragments from the server
- Places the response into
#user-list
That’s it. No fetch, no promises, no state management. The server returns HTML, and HTMX swaps it into the DOM.
Why HTMX Matters in 2026
The Problem with Modern Frontend
Modern Single Page Applications (SPAs) have introduced significant complexity:
| Issue | SPA Approach | HTMX Approach |
|---|---|---|
| State | Client manages state sync | Server is source of truth |
| Routing | Client-side router | Standard URLs, form submissions |
| Data Fetching | fetch() + JSON + state updates | HTML fragments |
| Build Tools | Webpack, Vite, bundling | None required |
| Bundle Size | 100KB - 500KB+ | ~14KB |
The SPA model made sense when browsers were slow and server roundtrips were expensive. Today, with HTTP/2 multiplexing and edge caching, the calculus has changed.
Hypermedia-Driven Applications
HTMX promotes Hypermedia-Driven Applications (HDAs) — where HTML is the communication medium, not JSON. This isn’t regression; it’s a return to RESTful principles with modern capabilities.
Related: The Modern Frontend Stack of 2026 explores how Astro embraces similar principles with islands architecture.
Core HTMX Attributes
Triggering Requests
HTMX provides attributes for all HTTP methods:
<!-- GET requests -->
<button hx-get="/api/data">Fetch Data</button>
<!-- POST requests (forms, actions) -->
<form hx-post="/api/users" hx-target="#result">
<input name="username" />
<button type="submit">Create User</button>
</form>
<!-- PUT and DELETE -->
<button hx-put="/api/users/1">Update</button>
<button hx-delete="/api/users/1">Delete</button>
Trigger Modifiers
Control when requests fire:
<!-- Trigger on events other than click -->
<input hx-get="/search" hx-trigger="keyup changed delay:500ms" />
<!-- Trigger on load -->
<div hx-get="/notifications" hx-trigger="load"></div>
<!-- Trigger every 30 seconds -->
<div hx-get="/live-data" hx-trigger="every 30s"></div>
<!-- Trigger on custom events -->
<div hx-get="/refresh" hx-trigger="refresh-data from:body"></div>
Target and Swap
Control where content goes and how it appears:
<!-- Target specific element -->
<button hx-get="/modal" hx-target="#modal-container">
Open Modal
</button>
<!-- Swap strategies -->
<div hx-get="/comments"
hx-swap="innerHTML"> <!-- default: replace content -->
<div hx-get="/comments"
hx-swap="outerHTML"> <!-- replace entire element -->
<div hx-get="/comments"
hx-swap="beforebegin"> <!-- insert before -->
<div hx-get="/comments"
hx-swap="afterend"> <!-- insert after -->
<div hx-get="/comments"
hx-swap="delete"> <!-- remove element -->
<!-- Morph DOM smoothly -->
<div hx-get="/list" hx-swap="morph:innerHTML"></div>
Building a Real App: Live Search
Let’s build a live search feature — typically requiring React state, effects, and debouncing. With HTMX, it’s declarative HTML:
<!-- search.html -->
<div class="search-container">
<input
type="text"
name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms, search"
hx-target="#results"
hx-indicator="#spinner"
placeholder="Search users..."
/>
<span id="spinner" class="htmx-indicator">Searching...</span>
</div>
<div id="results"></div>
The backend (Go example):
// main.go
package main
import (
"html/template"
"net/http"
"strings"
)
var users = []string{
"Alice Johnson", "Bob Smith", "Charlie Brown",
"Diana Prince", "Eve Williams", "Frank Miller",
}
func searchHandler(w http.ResponseWriter, r *http.Request) {
query := strings.ToLower(r.URL.Query().Get("q"))
var results []string
for _, user := range users {
if strings.Contains(strings.ToLower(user), query) {
results = append(results, user)
}
}
// Return HTML fragment
tmpl := `<ul class="search-results">
{{range .}}
<li class="result-item">{{.}}</li>
{{end}}
</ul>`
t := template.Must(template.New("results").Parse(tmpl))
t.Execute(w, results)
}
func main() {
http.HandleFunc("/api/search", searchHandler)
http.ListenAndServe(":8080", nil)
}
No JavaScript. No state management. The server is the source of truth.
Form Validation with HTMX
Server-side validation becomes trivial with HTMX:
<form hx-post="/api/register" hx-target="#form-result">
<div class="form-group">
<label>Email</label>
<input
name="email"
type="email"
hx-post="/api/validate-email"
hx-trigger="change"
hx-target="next .error"
/>
<span class="error"></span>
</div>
<div class="form-group">
<label>Username</label>
<input
name="username"
hx-post="/api/validate-username"
hx-trigger="change"
hx-target="next .error"
/>
<span class="error"></span>
</div>
<button type="submit">Register</button>
</form>
<div id="form-result"></div>
Backend returns HTML fragments for errors:
// Returns: <span class="error text-red">Email already registered</span>
// or: <span class="error text-green">✓ Available</span>
Infinite Scroll
Loading more content as the user scrolls:
<div id="posts-container">
<!-- Initial posts -->
<div class="post">Post 1</div>
<div class="post">Post 2</div>
<!-- Trigger next page load -->
<div
hx-get="/api/posts?page=2"
hx-trigger="revealed"
hx-swap="outerHTML"
>
Loading more...
</div>
</div>
Each page returns posts plus the next trigger:
<div class="post">Post 3</div>
<div class="post">Post 4</div>
<div
hx-get="/api/posts?page=3"
hx-trigger="revealed"
hx-swap="outerHTML"
>
Loading more...
</div>
WebSocket and SSE
Real-time updates without JavaScript:
<!-- Server-Sent Events -->
<div hx-ext="sse" sse-connect="/api/events">
<div sse-swap="message" hx-swap="innerHTML">
Waiting for updates...
</div>
</div>
<!-- WebSocket -->
<div
hx-ext="ws"
ws-connect="/api/chat"
>
<div id="messages" ws-swap="message"></div>
<form ws-send>
<input name="message" />
<button>Send</button>
</form>
</div>
Related: For real-time architectures with Flutter, see Building a Flutter AI Voice Assistant.
Loading States and UX
HTMX provides built-in indicators:
<style>
.htmx-request .htmx-indicator { display: inline; }
.htmx-request .normal-state { display: none; }
</style>
<button hx-get="/slow-endpoint">
<span class="normal-state">Submit</span>
<span class="htmx-indicator">Loading...</span>
</button>
Disable elements during requests:
<button
hx-get="/api/data"
hx-disabled-elt="this"
>
Load Data
</button>
Error Handling
Handle HTTP errors gracefully:
<div
hx-get="/api/data"
hx-target="#content"
hx-on::htmx:beforeRequest="console.log('Loading...')"
hx-on::htmx:responseError="alert('Request failed')"
>
Load Data
</div>
Or use htmx:afterRequest events:
<div
hx-get="/api/data"
hx-target="#content"
_="on htmx:responseError(alert 'Error: ' + event.detail.xhr.status)"
>
Load Data
</div>
The _ attribute uses hyperscript for simple client-side logic.
When to Use HTMX
HTMX Excels At:
- Content-driven sites — blogs, dashboards, admin panels
- Form-heavy applications — CRUD interfaces, wizards
- Teams with strong backend skills — leverage existing expertise
- Progressive enhancement — works without JavaScript
- Rapid prototyping — no build step, immediate feedback
HTMX May Not Fit:
- Highly interactive UIs — games, drag-and-drop editors
- Offline-first apps — need service workers and local state
- Complex client-side animations — Three.js, complex SVG
For offline-first architecture, see Local-First Software Architecture.
Backend Agnostic
HTMX works with any backend that returns HTML:
| Language | Framework |
|---|---|
| Go | Gin, Chi, Fiber |
| Python | Django, Flask, FastAPI |
| Ruby | Rails, Sinatra |
| Node.js | Express, Fastify |
| PHP | Laravel, Symfony |
| Rust | Axum, Actix |
Related: Building RESTful APIs with Go and Chi pairs well with HTMX frontend.
Security Considerations
HTMX doesn’t introduce new vulnerabilities, but remember:
- CSRF Protection — Include CSRF tokens in forms
- Input Validation — Always validate server-side
- Rate Limiting — HTMX triggers can be rapid
- CORS — Only needed for cross-origin requests
<form hx-post="/api/action">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}" />
<button>Submit</button>
</form>
Performance Comparison
Real-world metrics from a dashboard application:
| Metric | React SPA | HTMX |
|---|---|---|
| Initial Load | 340KB JS + 45KB CSS | 14KB HTMX + 12KB CSS |
| Time to Interactive | 2.1s | 0.4s |
| Server Requests | 1 initial + API calls | HTML fragments |
| Memory Usage | 45MB | 8MB |
The HTMX version loads 20x less JavaScript and becomes interactive 5x faster.
Getting Started
- Add HTMX via CDN:
<script src="https://unpkg.com/htmx.org@latest"></script>
-
Add attributes to your HTML elements
-
Return HTML fragments from your backend
-
Enjoy simplicity — no build step, no state management
For a deeper dive into the philosophy, read HTMX Essays — they challenge modern frontend assumptions with historical context.
Conclusion
HTMX represents a paradigm shift back to hypermedia principles. By letting HTML describe its own behavior, we eliminate the impedance mismatch between server and client. The server becomes the source of truth, state lives where it belongs, and complexity drops dramatically.
In 2026, with edge computing and HTTP/2 making server roundtrips faster than ever, HTMX offers a compelling alternative to JavaScript-heavy SPAs. Not every application needs React. Many applications just need HTML that knows how to update itself.
Less JavaScript, more web. 🚀
You might also like
API Gateway Patterns: The Front Door to Your Microservices
Master API Gateway patterns for microservices architecture. Learn request routing, authentication, rate limiting, and service mesh integration with TypeScript examples.
Database Connection Pooling: Patterns for High-Performance Applications
Master database connection pooling for scalable applications. Compare pgBouncer, HikariCP, and connection pool patterns with practical examples and performance benchmarks.
Building Resilient APIs: Circuit Breakers, Retries, and Rate Limiting in Production
Master production-ready API resilience with circuit breakers, exponential backoff, rate limiting, and fallback strategies. Includes TypeScript examples and real-world patterns.
More Posts
API Gateway Patterns: The Front Door to Your Microservices
Web Components 2026: Building Framework-Agnostic UI Libraries
Building Autonomous AI Workflows with LangGraph: A Practical Guide
Building Type-Safe APIs with tRPC in 2026: Full-Stack TypeScript Without Schemas
Database Connection Pooling: Patterns for High-Performance Applications
Prompt Caching: Reduce LLM Costs by 90% with Smart Context Management
Enjoyed This Post?
Want to discuss the topic, have questions, or looking to collaborate on something similar? Drop a comment below or reach out directly.
