htmx is the HTML-over-the-wire library that lets you build modern, interactive web apps with just HTML attributes — no JavaScript framework needed. 35K+ GitHub stars and growing.
Why htmx?
- No build step — just a script tag
- No JavaScript — HTML attributes for AJAX, WebSockets, SSE
- Works with any backend — Django, Rails, Express, Go, PHP
- 12KB gzipped — smaller than a React hello world
- Hypermedia-driven — the server returns HTML, not JSON
Quick Start
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
That's it. No npm install. No webpack. No bundler.
Core Concepts
<!-- Click button → GET /api/users → insert response into #user-list -->
<button hx-get="/api/users" hx-target="#user-list" hx-swap="innerHTML">
Load Users
</button>
<div id="user-list"></div>
<!-- Submit form → POST /api/contacts → append to list -->
<form hx-post="/api/contacts" hx-target="#contacts" hx-swap="beforeend">
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit">Add Contact</button>
</form>
<div id="contacts"></div>
Search (Live, No JavaScript)
<input
type="search"
name="q"
placeholder="Search users..."
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
/>
<div id="results"></div>
Server endpoint returns HTML:
# Flask example
@app.get("/api/search")
def search():
q = request.args.get("q", "")
users = User.query.filter(User.name.ilike(f"%{q}%")).all()
return render_template("partials/user_list.html", users=users)
Infinite Scroll
<div id="feed">
<!-- Existing items -->
<div class="post">Post 1</div>
<div class="post">Post 2</div>
<!-- This triggers when scrolled into view -->
<div hx-get="/api/posts?page=2"
hx-trigger="revealed"
hx-swap="afterend">
Loading...
</div>
</div>
Delete with Confirmation
<button
hx-delete="/api/users/123"
hx-confirm="Are you sure you want to delete this user?"
hx-target="closest tr"
hx-swap="outerHTML swap:500ms"
>
Delete
</button>
The row fades out and is removed from the DOM — no JavaScript.
Inline Editing
<!-- Display mode -->
<div id="user-name" hx-get="/api/users/1/edit" hx-trigger="click" hx-swap="outerHTML">
<span>Alice</span>
<small>(click to edit)</small>
</div>
<!-- Server returns edit form -->
<!-- /api/users/1/edit returns: -->
<form hx-put="/api/users/1" hx-target="this" hx-swap="outerHTML">
<input name="name" value="Alice" />
<button type="submit">Save</button>
<button hx-get="/api/users/1" hx-target="this" hx-swap="outerHTML">Cancel</button>
</form>
WebSocket Support
<div hx-ws="connect:/ws/chat">
<div id="messages"></div>
<form hx-ws="send">
<input name="message" placeholder="Type a message..." />
<button type="submit">Send</button>
</form>
</div>
Server-Sent Events
<div hx-sse="connect:/api/notifications">
<div hx-sse="swap:notification" id="alerts">
<!-- New notifications appear here automatically -->
</div>
<div hx-sse="swap:progress">
<!-- Progress updates stream here -->
</div>
</div>
Loading Indicators
<button hx-get="/api/report" hx-target="#report">
Generate Report
<span class="htmx-indicator">
<img src="/spinner.svg" /> Loading...
</span>
</button>
The indicator shows automatically during the request and hides when complete.
Express.js Backend Example
import express from "express";
const app = express();
app.use(express.urlencoded({ extended: true }));
let todos = [
{ id: 1, text: "Learn htmx", done: false },
];
app.get("/", (req, res) => {
res.send(`
<html>
<head>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
</head>
<body>
<h1>Todo App</h1>
<form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend" hx-on::after-request="this.reset()">
<input name="text" placeholder="New todo..." required />
<button type="submit">Add</button>
</form>
<ul id="todo-list">
${todos.map(t => `<li>${t.text}</li>`).join("")}
</ul>
</body>
</html>
`);
});
app.post("/todos", (req, res) => {
const todo = { id: Date.now(), text: req.body.text, done: false };
todos.push(todo);
// Return just the new HTML fragment
res.send(`<li>${todo.text}</li>`);
});
app.listen(3000);
Full interactive todo app. Zero client-side JavaScript written.
htmx vs React vs Alpine.js vs Stimulus
| Feature | htmx | React | Alpine.js | Stimulus |
|---|---|---|---|---|
| Size | 12KB | 40KB+ | 15KB | 20KB |
| Build step | None | Required | None | Optional |
| Learning curve | HTML only | JSX + hooks | HTML + JS | HTML + JS |
| Server rendering | Native | SSR (complex) | No | Turbo |
| JavaScript needed | None | Yes | Minimal | Minimal |
| State management | Server | Client | Client | Both |
| Best for | Server-rendered apps | SPAs | Sprinkles | Rails apps |
Need to scrape data from any website and get it in structured JSON? Check out my web scraping tools on Apify — no coding required, results in minutes.
Have a custom data extraction project? Email me at spinov001@gmail.com — I build tailored scraping solutions for businesses.
Top comments (0)