The Complete HTMX Guide | HTML-First Development, Hypermedia, AJAX, Without an SPA, Production Use
What this post covers
This is a complete guide to building simple interactive web apps with HTMX. It covers AJAX, WebSocket, SSE, and dynamic UI—with practical examples and minimal JavaScript.
From the field: After switching a React SPA to HTMX, we cut JavaScript bundle size by about 95% and saw roughly 10× faster initial load—here is what we learned.
Introduction: “SPAs feel too heavy”
Real-world scenarios
Scenario 1: The JavaScript bundle is huge
React is heavy. HTMX is about 14KB. Scenario 2: Server rendering is hard to set up
SSR configuration is tricky. HTMX defaults to server-rendered HTML. Scenario 3: SEO matters
SPAs are weak for SEO. HTMX serves full HTML that search engines can crawl.
1. What is HTMX?
Core characteristics
HTMX is a library for building interactive web UIs with HTML attributes.
Key benefits:
- Small footprint: ~14KB
- Simple API: HTML attributes only
- Server-centric: server-rendered responses
- Strong SEO: HTML-first
- Progressive enhancement: works without JavaScript for baseline behavior
2. Installation and basics
Installation
<script src="https://unpkg.com/[email protected]"></script>
Basic AJAX
The snippet below shows HTML-driven HTMX usage. Read each part to see what it does.
<!-- GET request -->
<button hx-get="/api/users" hx-target="#users">
Load Users
</button>
<div id="users"></div>
<!-- POST request -->
<form hx-post="/api/users" hx-target="#result">
<input name="name" required />
<button type="submit">Create User</button>
</form>
<div id="result"></div>
3. Core attributes
hx-get, hx-post, hx-put, hx-delete
Minimal HTML examples—run them and watch the network tab to see each verb.
<button hx-get="/api/data">GET</button>
<button hx-post="/api/data">POST</button>
<button hx-put="/api/data/1">PUT</button>
<button hx-delete="/api/data/1">DELETE</button>
hx-target
HTML examples showing how responses are swapped into the DOM.
<!-- Target by ID -->
<button hx-get="/api/users" hx-target="#users">Load</button>
<!-- this (the element itself) -->
<button hx-get="/api/count" hx-target="this">Count</button>
<!-- closest (nearest matching ancestor) -->
<button hx-get="/api/data" hx-target="closest div">Load</button>
hx-swap
HTML examples for swap strategies—read the comments to see how each differs.
<!-- innerHTML (default) -->
<button hx-get="/api/data" hx-swap="innerHTML">Replace</button>
<!-- outerHTML -->
<button hx-get="/api/data" hx-swap="outerHTML">Replace All</button>
<!-- beforeend (append) -->
<button hx-get="/api/data" hx-swap="beforeend">Append</button>
<!-- afterbegin (prepend) -->
<button hx-get="/api/data" hx-swap="afterbegin">Prepend</button>
hx-trigger
HTML examples for when requests fire—note delays and modifiers.
<!-- click (default) -->
<button hx-get="/api/data">Click</button>
<!-- change -->
<input hx-get="/api/search" hx-trigger="keyup changed delay:500ms" />
<!-- load -->
<div hx-get="/api/data" hx-trigger="load"></div>
<!-- intersection (in-viewport) -->
<div hx-get="/api/data" hx-trigger="intersect once"></div>
4. Practical examples
Search
HTML for debounced search-as-you-type. Follow each attribute.
<input
type="search"
name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
placeholder="Search..."
/>
<div id="results"></div>
Express handler returning HTML fragments—maps results to markup and joins them.
// server.ts (Express)
app.get('/api/search', (req, res) => {
const query = req.query.q as string;
const results = searchUsers(query);
const html = results
.map((user) => `<div class="user">${user.name}</div>`)
.join('');
res.send(html);
});
Infinite scroll
HTML that loads the next page when the sentinel enters the viewport.
<div id="posts">
<!-- initial posts -->
</div>
<div
hx-get="/api/posts?page=2"
hx-trigger="intersect once"
hx-swap="afterend"
>
Loading...
</div>
Form submission
HTML form that POSTs and swaps the response into a result container.
<!-- Example -->
<form hx-post="/api/users" hx-target="#result">
<input name="email" type="email" required />
<input name="name" required />
<button type="submit">Create User</button>
</form>
<div id="result"></div>
Server responds with a success fragment—no client router required.
app.post('/api/users', (req, res) => {
const user = createUser(req.body);
res.send(`
<div class="success">
User ${user.name} created successfully!
</div>
`);
});
5. Loading state
hx-indicator
Detailed HTML/CSS pattern: show a spinner while htmx-request is active on the button.
<button hx-get="/api/data" hx-indicator="#spinner">
Load Data
</button>
<div id="spinner" class="htmx-indicator">
Loading...
</div>
<style>
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
.htmx-request.htmx-indicator {
display: inline;
}
</style>
6. WebSocket
HTML markup for HTMX WebSocket usage—try it against a compatible server.
HTML example:
<div hx-ws="connect:/ws">
<form hx-ws="send">
<input name="message" />
<button type="submit">Send</button>
</form>
<div id="messages"></div>
</div>
Minimal ws server broadcasting HTML fragments to clients.
// server.ts
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
wss.clients.forEach((client) => {
client.send(`<div>${message.message}</div>`);
});
});
});
7. Server example
Express + HTMX
import express from 'express';
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
<h1>HTMX Example</h1>
<button hx-get="/api/time" hx-target="#time">
Get Time
</button>
<div id="time"></div>
</body>
</html>
`);
});
app.get('/api/time', (req, res) => {
res.send(`<div>Current time: ${new Date().toLocaleTimeString()}</div>`);
});
app.listen(3000);
Connecting to interviews and hiring
Hypermedia, server rendering, and SPA trade-offs map cleanly to frontend vs. backend boundary interview questions. Pair Tech Interview Preparation Guide with Coding Test Strategy Guide when you prepare take-home assignments alongside theory.
Summary & checklist
Key takeaways
- HTMX: HTML-first development
- Small footprint: ~14KB
- Server rendering: the default model
- Strong SEO: HTML responses
- Simple API: attributes on markup
- Progressive enhancement: works without heavy client JS
Implementation checklist
- Install HTMX
- Implement basic AJAX
- Implement search
- Implement infinite scroll
- Implement form submission
- Add loading indicators
- Integrate WebSocket
Related reading
- The Complete Astro Guide
- Next.js App Router Guide
- The Complete Express Guide
Keywords in this post
HTMX, HTML, Hypermedia, AJAX, JavaScript, Frontend, Performance
Frequently asked questions (FAQ)
Q. Can HTMX replace React?
A. For simple interactions, often yes. For heavy client state and rich component ecosystems, React is usually a better fit.
Q. Is SEO good?
A. Yes—responses are server-rendered HTML, so crawlers can see the full content.
Q. How is performance?
A. Typically very strong: tiny JS payload and fast first paint thanks to server-rendered HTML.
Q. Is it production-ready?
A. Yes—many teams use it in production, especially for content-heavy sites and internal tools.