TL;DR: htmx makes your websites interactive without React, Angular, or Vue. A few HTML attributes replace tons of JavaScript while still delivering dynamic UIs. Here's how it works and when it makes sense.
🤔 What Is htmx, Anyway?
Imagine making your website interactive without writing a single line of JavaScript. Sounds too good to be true? That's exactly what htmx does.
htmx is a small JavaScript library (~14 KB gzipped) that extends HTML with new attributes. You can fire AJAX requests, swap DOM elements, and even use WebSockets — all directly from HTML. The concept behind it is called "HTML over the wire" — instead of shipping JSON back and forth, the server returns ready-to-render HTML.
The philosophy is radically simple: HTML has always been a hypertext language. Why should only <a> and <form> be allowed to make HTTP requests? htmx gives every element that ability.
🚀 Why the World Needs htmx
Let's be honest: are you tired of spinning up a full-blown frontend framework for every tiny interaction? React, Angular, Vue — all great, but sometimes you're bringing a sledgehammer to hang a picture frame.
JavaScript framework fatigue is real. You just want to submit a form without a page reload? You don't need a 200 KB framework with a build pipeline, state management, and Virtual DOM for that. htmx says: "Hold on. There's a simpler way."
The best part: htmx works with your server, not against it. Your backend renders HTML — exactly what servers have been doing for 30 years. No API layer, no JSON serialization, no frontend state management.
⚙️ Core Attributes — Your New Toolkit
htmx extends HTML with a handful of attributes that pack a serious punch. Here are the key ones:
HTTP Request Attributes
hx-get, hx-post, hx-put, and hx-delete do exactly what you think — they fire the corresponding HTTP request:
<button hx-get="/api/users" hx-target="#user-list">
Load Users
</button>
<div id="user-list">
<!-- Results will appear here -->
</div>One click on the button, and the content from /api/users gets injected into #user-list. No JavaScript. No fetch(). No setState().
hx-target — Where Should It Go?
hx-target determines which element receives the response HTML. You can use CSS selectors:
<button hx-get="/notifications" hx-target="#notification-bell">
Check Notifications
</button>hx-swap — How to Insert?
hx-swap controls how the new content gets inserted:
| Value | Description |
|---|---|
innerHTML | Replaces inner content (default) |
outerHTML | Replaces the entire element |
beforebegin | Inserts before the element |
afterend | Inserts after the element |
beforeend | Appends inside the element |
afterbegin | Prepends inside the element |
delete | Deletes the element |
none | No swap, just the request |
hx-trigger — When to Fire?
hx-trigger determines which event triggers the request. By default, it's click for buttons and change for inputs:
<input type="text" name="search"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results">That delay:300ms is debouncing out of the box. Just like that. No lodash.debounce needed.
💻 Practical Examples — htmx in Action
Live Search with Results
A search bar that shows results as you type — in three lines of HTML:
<input type="search" name="q"
hx-get="/search"
hx-trigger="input changed delay:300ms"
hx-target="#results"
hx-indicator="#spinner"
placeholder="Search...">
<span id="spinner" class="htmx-indicator">🔄 Searching...</span>
<div id="results"></div>The server simply responds with HTML fragments:
<!-- Server response for /search?q=htmx -->
<ul>
<li>htmx - high power tools for HTML</li>
<li>htmx vs React - a comparison</li>
</ul>Infinite Scroll
Endless scrolling without a single line of JavaScript:
<div id="articles">
<article>Article 1...</article>
<article>Article 2...</article>
<div hx-get="/articles?page=2"
hx-trigger="revealed"
hx-swap="outerHTML">
Loading more...
</div>
</div>hx-trigger="revealed" fires as soon as the element scrolls into the viewport. The server returns the next batch of articles plus a new trigger for page 3. Beautifully simple.
Form Submission Without Page Reload
<form hx-post="/contact" hx-target="#form-response" hx-swap="outerHTML">
<input type="text" name="name" placeholder="Your Name" required>
<input type="email" name="email" placeholder="Your Email" required>
<textarea name="message" placeholder="Your Message"></textarea>
<button type="submit">Send</button>
</form>
<div id="form-response"></div>Delete with Confirmation
<button hx-delete="/users/42"
hx-confirm="Are you sure you want to delete this user?"
hx-target="closest tr"
hx-swap="outerHTML swap:500ms">
🗑️ Delete
</button>hx-confirm shows a native browser dialog. The swap:500ms gives you time for a CSS transition before the element disappears.
🎨 hx-indicator — Loading States Made Easy
Users need feedback. With hx-indicator, you can show loading states without JavaScript:
<button hx-get="/slow-endpoint" hx-indicator="#loading">
Load Data
</button>
<span id="loading" class="htmx-indicator">
⏳ Please wait...
</span>.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}htmx automatically adds the htmx-request class while a request is in flight. Done.
🔧 hx-boost — Progressive Enhancement on Steroids
This one's my personal favorite. hx-boost turns regular links and forms into AJAX requests — without changing anything:
<body hx-boost="true">
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main id="content">
<!-- Page content -->
</main>
</body>Every link inside hx-boost gets intercepted and loaded via AJAX. The URL in the browser still changes (History API). And if JavaScript is disabled? Everything works as regular navigation. THAT is progressive enhancement.
🎯 htmx vs. React/Angular/Vue — When to Use Which?
Now for the real talk. htmx isn't always the right choice. Here's an honest comparison:
| Criteria | htmx | React/Angular/Vue |
|---|---|---|
| Bundle Size | ~14 KB gzipped | 40-200+ KB |
| Learning Curve | Minimal (HTML knowledge is enough) | Steep (JSX, Hooks, RxJS, etc.) |
| Build Pipeline | Not needed | Webpack/Vite/etc. required |
| Real-time Interactivity | Good for simple cases | Better for complex UIs |
| Offline Capability | Difficult | Good (Service Workers, PWA) |
| SEO | Excellent (server-rendered) | Needs SSR/SSG |
| Team Scaling | Simple | Better for large teams |
Use htmx when:
- You're building content-heavy websites (blogs, CMS, e-commerce)
- You need quick prototypes
- Your team is mostly backend developers
- Progressive enhancement matters to you
- You don't need complex client-side logic
Use React/Angular/Vue when:
- You're building complex SPAs (dashboards, editors, chat apps)
- You need offline functionality
- You have a large frontend team
- You need extensive client-side state management
🛡️ Integration with Backend Frameworks
htmx is backend-agnostic. Your server just needs to return HTML. Here are some examples:
Express / NestJS
// Express Route
app.get('/search', (req, res) => {
const query = req.query.q;
const results = searchDatabase(query);
// Just return HTML!
res.send(`
<ul>
${results.map(r => `<li>${r.title}</li>`).join('')}
</ul>
`);
});
// NestJS Controller
@Controller('search')
export class SearchController {
@Get()
search(@Query('q') query: string) {
const results = this.searchService.find(query);
return results.map(r => `<li>${r.title}</li>`).join('');
}
}Django
# views.py
from django.shortcuts import render
def search(request):
query = request.GET.get('q', '')
results = Article.objects.filter(title__icontains=query)
if request.headers.get('HX-Request'):
# htmx request — return just the fragment
return render(request, 'partials/search_results.html',
{'results': results})
# Regular request — full page
return render(request, 'search.html',
{'results': results, 'query': query})The HX-Request header is brilliant — it lets you detect whether a request comes from htmx, so you can return just an HTML fragment instead of the full page.
⚠️ Limitations — When NOT to Use htmx
As much as I love htmx, there are clear limits:
- Complex client-side logic: Drag & drop editors, real-time collaboration, image editing — for these, you need real JavaScript.
- Offline-first apps: htmx always needs a server connection. No server, no interaction.
- High-frequency UI updates: If you need 60fps animations or real-time data visualizations, a Virtual DOM is more efficient.
- Complex state: When your UI depends on many interdependent states, things get messy with htmx quickly.
- Mobile apps: htmx isn't designed for React Native or similar frameworks.
But hey — for most websites out there, these points are irrelevant. Not every website is Google Docs.
🚀 Browser Support and Bundle Size
htmx supports all modern browsers: Chrome, Firefox, Safari, Edge. IE11 is no longer supported since htmx 2.x — but let's be real, it's 2025, IE11 should be long gone.
Bundle size? About 14 KB gzipped. For comparison: React + ReactDOM are ~45 KB gzipped. Angular starts at ~65 KB. You save massively on bandwidth and load time.
How to include it? One script tag:
<script src="https://unpkg.com/[email protected]"></script>No npm install. No build step. No node_modules folder with 500 MB. Just include it and go.
💡 Conclusion
htmx isn't a revolution — it's a return to basics. Back to what HTML was always meant to be: a powerful hypertext language. Instead of building ever more complex JavaScript frameworks, htmx extends HTML with the missing capabilities.
For content websites, dashboards, admin panels, and most CRUD applications, htmx is a serious alternative to React & friends. You write less code, don't need a build pipeline, and your pages are faster.
My tip: just try it on your next project. You'll be surprised how far you can get without a single npm install.