The modern frontend default is a heavy JavaScript Single-Page Application: React, Next.js, a state management library, a bundler, hydration, and tens of kilobytes of JavaScript shipped before your user sees anything meaningful. For many applications, this is overkill.
htmx takes a radically different position: it extends HTML with new attributes that let the browser make AJAX requests, update parts of the DOM, and handle user interactions — without writing a single line of JavaScript. Instead of maintaining a client-side state machine, you simply render HTML on the server in response to every interaction.
In 2026, htmx is experiencing significant adoption among developers building server-centric applications with Python (Django, FastAPI), Ruby on Rails, Go, and even Node.js backends.
What htmx Does
htmx extends HTML with a small set of attributes:
<!-- Make this button trigger a POST request and replace the #result div -->
<button
hx-post="/api/toggle-like"
hx-target="#like-count"
hx-swap="outerHTML"
hx-include="[nam='post-id']"
>
♡ Like
</button>
<input type="hidden" name="post-id" value="123">
<span id="like-count">42 likes</span>When clicked, htmx POSTs to /api/toggle-like, takes the HTML response, and replaces the #like-count element — no JavaScript written, no state to manage.
The server returns a fragment of HTML:
<!-- Server response for POST /api/toggle-like -->
<span id="like-count">43 likes</span>That's it. The full interaction cycle.
Core htmx Attributes
| Attribute | Purpose | Example |
|---|---|---|
hx-get | Trigger GET request | hx-get="/search?q=..." |
hx-post | Trigger POST request | hx-post="/api/comment" |
hx-target | CSS selector of element to update | hx-target="#results" |
hx-swap | How to swap: innerHTML, outerHTML, beforeend, afterend | hx-swap="beforeend" |
hx-trigger | What triggers the request | hx-trigger="keyup delay:300ms" |
hx-include | Include additional form inputs | hx-include="[name='csrf']" |
hx-push-url | Update browser URL on request | hx-push-url="true" |
hx-indicator | Show loading indicator | hx-indicator="#spinner" |
Real Examples
Live Search
<!-- Search that queries as you type, with 300ms debounce -->
<input
type="search"
name="q"
placeholder="Search products..."
hx-get="/api/products/search"
hx-trigger="keyup delay:300ms changed"
hx-target="#search-results"
hx-indicator="#search-spinner"
>
<div id="search-spinner" class="htmx-indicator">
<span>Searching...</span>
</div>
<div id="search-results">
<!-- Server returns product card HTML fragments here -->
</div>Server handler (Next.js):
// app/api/products/search/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const q = searchParams.get('q') ?? '';
const products = await db.query(
'SELECT * FROM products WHERE name ILIKE $1 LIMIT 10',
[`%${q}%`]
);
// Return HTML fragment, not JSON
const html = products.rows.map(p => `
<div clas="product-card">
<h3>${p.name}</h3>
<p>$${p.price}</p>
</div>
`).join('');
return new Response(html, { headers: { 'Content-Type': 'text/html' } });
}Infinite Scroll
<!-- Last item triggers loading the next page -->
<div class="post-list" id="posts">
<!-- Posts render here -->
<!-- The last item triggers the next page load -->
<div
hx-get="/api/posts?pag=2"
hx-trigger="revealed"
hx-swap="outerHTML"
hx-target="this"
>
Loading more...
</div>
</div>Delete with Confirmation
<article id="post-42">
<h2>My Blog Post</h2>
<button
hx-delete="/api/posts/42"
hx-target="#post-42"
hx-swap="outerHTML swap:0.5s"
hx-confirm="Are you sure you want to delete this post?"
>
Delete
</button>
</article>The server responds with an empty string or a notification HTML fragment, and htmx removes the article element.
When htmx Is the Right Choice
htmx shines in specific contexts:
✅ Server-rendered applications where the backend already generates HTML (Django, Rails, Laravel, Go templates, Next.js with RSC).
✅ CRUD-heavy admin panels and dashboards where forms, tables, and modals are the primary interaction pattern.
✅ Content-heavy sites (blogs, documentation, news) with some interactive enhancements.
✅ Team skill constraints — backend developers who are less comfortable with React can build full interactive features.
✅ Performance-critical applications — htmx ships as a single 14KB script with zero dependencies.
When htmx Is NOT the Right Choice
❌ Highly stateful client interfaces — complex drag-and-drop, real-time collaborative editing, or applications with intricate client-side state graphs.
❌ Offline-first applications — htmx requires a server response for every interaction.
❌ Large teams with established React expertise — the productivity cost of switching outweighs the simplicity benefit.
❌ Complex client-side data transformations — if your frontend logic involves significant data processing before display, a proper client-side framework handles this better.
htmx with Next.js
htmx works with any HTTP server, including Next.js:
// app/page.tsx — Server Component with htmx
import Script from 'next/script';
export default function Page() {
return (
<>
<Script src="https://unpkg.com/htmx.org@2.0.0" />
<form hx-post="/api/newsletter" hx-target="#form-response" hx-swap="innerHTML">
<input type="email" name="email" placeholder="your@email.com" required />
<button type="submit">Subscribe</button>
</form>
<div id="form-response"></div>
</>
);
}// app/api/newsletter/route.ts — Returns HTML fragment
export async function POST(request: Request) {
const formData = await request.formData();
const email = formData.get('email') as string;
await subscribeToNewsletter(email);
return new Response(
'<p clas="success">✓ Subscribed successfully!</p>',
{ headers: { 'Content-Type': 'text/html' } }
);
}Comparing Bundle Sizes
| Approach | JavaScript Shipped | Interaction Model |
|---|---|---|
| htmx | ~14KB | Server-rendered HTML fragments |
| Alpine.js | ~16KB | Declarative JS in HTML attributes |
| Vanilla JS | Varies | Manual DOM manipulation |
| React + Next.js | ~120KB+ | Client-side state + virtual DOM |
| Vue | ~90KB+ | Client-side reactive state |
Conclusion
htmx is not a replacement for React for all use cases. It is a powerful tool for the substantial category of web applications that do not actually need client-side state management — applications where the server already knows everything and the UI is primarily a styled view of that server state. If your application fits this description, htmx gives you dynamic, interactive interfaces with dramatically less JavaScript, simpler architecture, and significantly better initial load performance. In 2026, reaching for a full SPA framework by default for every project is a choice worth questioning.