Delete Half Your Frontend: Where HTMX Replaces Your Framework
The Server Is the Framework: HTMX for Modern Web UX. How to decide when HTMX replaces React/Next/Nuxt/SvelteKit, where it fits alongside them, and how to build real, resilient apps with SSE, WebSockets, history, caching, and security.
If you strip front-end development down to its engine, you get two questions:
- How does user intent cross the network?
- How does the DOM change in response?
For a decade we answered both with JavaScript frameworks. HTMX answers them with HTML. Not nostalgia—architecture. By shipping HTML fragments over HTTP and letting attributes on elements describe network behavior, you move logic back to the place the web was built to optimize: servers rendering documents and partial documents. HTMX then adds just enough client power (AJAX, SSE, WebSockets, CSS transitions) through hx-* attributes to make modern interfaces feel instant without building a client operating system in your bundle. The official docs say as much: HTMX "lets you access modern browser features directly in HTML," and it's intentionally small (≈16.6 KB gzipped in the 2.x line).
In 2024–2025 HTMX matured from "clever hack" to serious choice. Version 2.0 dropped IE support, moved real-time transports into first-class extensions, and tightened defaults; the team then openly reconsidered history-snapshotting, advocating simpler, request-on-back semantics to avoid brittle local caching. These are not cosmetic tweaks; they are architecture moves.
This guide is for the moment you look at your next project and ask: Should I reach for React/Next/Nuxt/SvelteKit—or can I do more with less by using HTMX?
We'll define project sizes, make the call decisively, and show patterns, pitfalls, and a production-grade example with SSE and WebSockets.
---
Quick Decision Framework
Use HTMX when:- Your "interactivity" is mostly request/response (links, forms, partial updates)
- You want server-rendered truth with one validation path, one cache
- You need real-time but not a client OS (SSE/WebSockets via attributes)
- You want URL-addressable UI without a client router
- Offline-first is mandatory (client store, sync, conflict resolution)
- You have a truly stateful, multi-pane client application (Figma, Notion-level editors)
- You're building a component platform to share across many teams
- API Transport in 2025: Streaming Reality — How streaming APIs pair with HTML-over-the-wire
- Rust + WebAssembly: The Five-Millisecond Cloud — Edge compute that serves HTMX fragments
---
What HTMX Actually Does (and What It Doesn't)
The core idea: use HTML attributes to issue HTTP requests and swap returned HTML into the current page.
Attributes You'll Use Every Day
hx-get,hx-post,hx-put,hx-delete: send requests and swap the responsehx-swap: control how content is inserted (innerHTML,outerHTML,beforeend,afterbegin, etc.)hx-trigger: decide when requests fire (click,change,every 5s, or custom events)hx-target: choose what part of the page to updatehx-boost: turn links and forms into AJAX navigation with graceful fallbackhx-push-url: update the URL/history when swapping content
Real-Time via Extensions
- SSE via
hx-ext="sse",sse-connect="/events", andsse-swap="message"for one-way server→client updates over HTTP - WebSockets via
hx-ext="ws",ws-connect="/socket", andws-sendon forms for bidirectional updates
Out-of-Band Swaps
hx-swap-oob lets the server update other parts of the DOM piggy-backed on any response (e.g., update a badge count while returning a dialog).
Server Awareness
HTMX sends headers like HX-Request: true, HX-Boosted, HX-Target, and supports response headers such as HX-Redirect, HX-Location, and HX-Push-Url so your server can return fragments vs full pages and still control navigation.
What HTMX Doesn't Do
Client-side state management à la Redux, a virtual DOM, component composition primitives, or a client router. For "glue code" on the client, use minimal companions like hyperscript or Alpine.js—both work well with HTMX when you do need local state or DOM logic in the browser.
---
2025 Reality Check: HTMX 2.x and the "Fetchening"
As of mid-2025, HTMX 2.x is current (2.0.6 noted publicly). 2.x ended IE support, removed old inline hx-sse/hx-ws in favor of the above extensions, and made history behavior more explicit (and, increasingly, simpler). If you were burned by snapshot caches in 1.x/early-2.x back/forward flows, the maintainers now point you toward disabling snapshots or letting navigation refetch content—predictable, safer, and easier with today's servers.
---
Deciding: Small, Medium, Large (Be Honest About Scope)
Most teams overestimate project size. Use this:
Small (1–8 screens, low coupling)
CRUD tables, search & filters, forms, auth, a dashboard, a couple of modals. No offline requirements. Minimal cross-screen client state.
Default: HTMX + your server templating. Add Alpine/hyperscript for local UI state. No framework build step. Team: 1–3 engineers, 2–8 week delivery horizon.Medium (8–30 screens, moderate coupling)
Admin suites, CMS back-offices, subscription sites, multi-user dashboards, some real-time. Limited offline. A few rich components (calendar, charts).
Bias: HTMX first. Usehx-boost for navigation, hx-push-url for history, SSE for live updates, and drop in a reactive micro-lib (Alpine/hyperscript) where local state is heavy. Keep URLs addressable and the server the source of truth. Add a sprinkle of SPA in a single page only if required.
Team: 3–12 engineers, 2–6 month delivery in iterations.
Large (30+ screens, heavy coupling)
Complex product suites, dense client interactions, offline, embedded editors, multi-pane apps with drag-drop and large in-memory models.
Bias: Use a framework (React/Next, Nuxt/Vue, SvelteKit) for the hot zone that genuinely needs a client application. Use HTMX elsewhere—marketing, docs, account pages, settings, billing UI. Your platform becomes hybrid. The wrong move is not choosing HTMX; it's using one tool across wildly different problem shapes.---
When HTMX Replaces a Framework (and When It Shouldn't)
It Does Replace a Framework When...
- Your "interactivity" is mostly request/response. Links, forms, and partial updates dominate. HTMX attributes cover 90% of what you'd write client code for.
- You want server-rendered truth. You prefer one validation, one rendering path, one cache. HTMX's
HX-Requestlets your server return partial HTML for fragments and full pages otherwise. - You need real-time but not a client OS. SSE and WebSockets are straightforward and attribute-driven.
- You want URL-addressable UI without a client router.
hx-boostandhx-push-urlkeep history honest.
It Does Not Replace a Framework When...
- Offline-first is mandatory. You will need a client store, sync, conflict resolution, service worker orchestration—outside HTMX's charter.
- You have a truly stateful, multi-pane client application (think Figma, Notion-level editors). Hydration and client state orchestration are your main problems. Consider React Server Components/Next, Nuxt, or SvelteKit.
- You're building a component platform to share across many teams. Framework ecosystems are still strongest for that class of problem.
The Healthy Hybrid
HTML-over-the-wire is not unique to HTMX—Hotwire/Turbo does it in Rails, Symfony, Django, and others. The pattern is stable and mainstream. Many products mix: HTMX for most pages; a single framework "island" for an embedded editor or visualization heavy area.
---
Architecture Patterns That Actually Ship
1) Progressive Navigation with hx-boost
Wrap your main content in hx-boost="true" and let links/forms become AJAX requests that swap into . The page still works with JS off.
<main id="app" hx-boost="true" hx-target="#app">
<a href="/invoices">Invoices</a>
<form action="/search" method="get">
<input name="q">
</form>
</main>
Boosted links issue a GET, push a history entry, target the body (or your chosen hx-target), and fall back gracefully.
2) Make State URL-Addressable with hx-push-url
<div id="results"
hx-get="/reports?period=Q2"
hx-push-url="true"
hx-target="#results"
hx-trigger="change from:#period-select">
</div>
hx-push-url updates the address bar and instructs HTMX to handle history. Users can bookmark and share filtered states.
3) Real-Time Without a SPA: SSE and WebSockets
SSE for server→client feeds (stock ticks, progress, notifications):<div id="feed" hx-ext="sse" sse-connect="/events" sse-swap="message"></div>
SSE runs over HTTP, traverses proxies cleanly, and is uni-directional. The extension handles reconnection and named events.
WebSockets when the browser also needs to talk back:<div hx-ext="ws" ws-connect="/ws/room">
<div id="chat"></div>
<form ws-send>
<input name="message">
</form>
</div>
Inbound messages can include hx-swap-oob snippets to update targets anywhere on the page; the extension queues outbound messages and reconnects with jitter.
4) Out-of-Band Updates for "While You're Here..."
Return the main fragment and a badge update in one response:
<div id="modal">...</div>
<div id="alerts" hx-swap-oob="true">Saved!</div>
The server's response updates both the modal and the alerts region.
5) Client Behavior: Use hyperscript/Alpine Sparingly
- hyperscript favors tiny, readable behaviors inline:
_="on click toggle .open on #menu" - Alpine.js brings small, reactive state when you need it (dropdowns, tabs), and there's an
alpine-morphextension to preserve Alpine state across swaps
---
Performance, Payload, and Perceived Speed
- Bundle size: HTMX 2.x is ≈16–17 KB gzipped (plus any extension you use). For many apps that replaces 100–300 KB of framework + router + state management + hydration code.
- Cold starts: With no framework boot/hydration, interactive time is often "HTML parsed" + "one small script eval."
- Network: You're shipping HTML fragments, not JSON + client templates. That saves duplication (no "render on server then render again on client") and lets CDNs cache fragments aggressively.
Hotwire/Turbo documents the same wins for "HTML over the wire"—server renders, client swaps—across Drive/Frames/Streams; the approach is broadly adopted beyond Rails.
---
History, Caching, and the 2025 Guidance
HTMX historically snapshot DOM to navigate back instantly. In practice, third-party scripts and browser quirks make snapshots brittle. The maintainers now recommend disabling per-URL snapshotting with hx-history="false" (or globally) and letting the browser refetch content on back/forward. You still get correct history, but fewer heisenbugs and smaller client state attack surface.
---
Security: XSS, CSRF, and Fragment Correctness
- XSS: You're rendering HTML from the server; escape untrusted data at the template boundary and keep your templating engine's auto-escape on. OWASP's XSS guidance applies 1:1 here.
- CSRF: Include CSRF tokens in POST/PUT/PATCH/DELETE. With Django, for example, use
csrf_tokenin forms or add the token viahx-headers. - File downloads: Because HTMX uses XHR/fetch, you generally redirect the browser (
HX-Redirect) to a non-AJAX endpoint for actual downloads.
---
Compare to Modern Frameworks (2025 Edition)
- React / Next.js: Server Components remove client data-fetch code and reduce hydration, but you still maintain a component graph, boundaries, and bundling; you adopt conventions for streaming and cache layers; and you choose client vs server component carefully. This is powerful, but heavy if your UI is mostly classic hypermedia.
- Nuxt (Vue) and SvelteKit: Offer similarly excellent SSR/ISR/hybrid stories and are great when you need rich client state and large component libraries.
- Hotwire/Turbo: The closest cousin—a Rails-native set of tools (Drive/Frames/Streams) for HTML-over-the-wire with good real-time primitives and a "SPA-feel without SPA-bloat." HTMX gives you the same idea in a framework-agnostic, attribute-first style.
---
A Real-World Example: Live Orders (SSE + WebSockets), Zero Framework
Scenario
You're building a small–medium "Live Orders" console for a cafe chain. Requirements:
- Paginated order list with filters and URL-shareable state
- Real-time "new order" row injection
- Button to accept an order and see the badge count drop
- Broadcasted status changes (kitchen display updates everyone)
1) Layout and Boosted Navigation
<body>
<header>
<a href="/orders" hx-boost="true">Orders</a>
<a href="/reports" hx-boost="true">Reports</a>
<span id="badge" class="badge">0</span>
</header>
<main id="app" hx-boost="true" hx-target="#app">
<!-- server renders full page on first hit -->
<!-- subsequent clicks fetch fragments into #app -->
{{ template "orders/index.html" . }}
</main>
<script src="/static/htmx.min.js"></script>
<script src="/static/sse.js"></script>
<script src="/static/ws.js"></script>
</body>
hx-boost turns navigation into AJAX swaps with history updates; hx-target="#app" constrains swaps.
2) Filter + URL State
<form id="filters"
hx-get="/orders"
hx-target="#orders"
hx-push-url="true"
hx-trigger="change delay:300ms from:#status,#search">
<select id="status" name="status">
<option value="">All</option>
<option>NEW</option><option>PREP</option><option>READY</option>
</select>
<input id="search" name="q" placeholder="Search...">
</form>
<div id="orders">
{{ template "orders/table.html" . }}
</div>
The server returns a fragment for #orders when it sees HX-Request: true, and a full page otherwise.
3) Real-Time Updates (SSE)
<div id="realtime"
hx-ext="sse"
sse-connect="/events/orders"
sse-swap="message"></div>
Your SSE stream emits named or default message events containing HTML rows. HTMX swaps them into the right places (e.g., using hx-swap-oob="beforeend" on rows targeting #orders tbody).
event: message
data: <tr id="order-482" hx-swap-oob="beforeend" hx-target="#orders tbody">
<td>#482</td><td>Latte</td><td>NEW</td>
</tr>
4) Accept Order (WebSocket Round-Trip)
<div hx-ext="ws" ws-connect="/ws/orders">
<table id="orders-table">...</table>
<!-- Each row includes a small form that sends JSON to the socket -->
<form ws-send onsubmit="return false">
<input type="hidden" name="action" value="accept">
<input type="hidden" name="order_id" value="{{ .ID }}">
<button>Accept</button>
</form>
</div>
Server receives {action:"accept", order_id:482}, updates status, and broadcasts two fragments:
- The row with new status (OOB swap on
#order-482) - The badge count (OOB swap on
#badge)
<tr id="order-482" hx-swap-oob="true">
<td>#482</td><td>Latte</td><td>PREP</td>
</tr>
<span id="badge" hx-swap-oob="true">7</span>
5) History Correctness
When filters change (hx-push-url="true"), HTMX pushes a new state and can snapshot or refetch on back/forward. In 2025, favor refetch (disable snapshots where needed) for fewer brittle states.
6) QA Checklist for Production
- 404/401/422 handling: Map response codes to swaps or messages (HTMX lets you configure response handling and trigger client events after swap/settle)
- CSRF: Add token via hidden input or
hx-headers. In Django,{% csrf_token %}plus middleware is enough - Access control: SSE and WS endpoints must enforce auth and tenant scoping per connection
- Backpressure: Throttle SSE/WS streams per user; queue only what the UI consumes
- Caching: Cache HTML fragments at the edge; purge on update
- A11y: Ensure swapped regions are in live regions (
role="status"/aria-live="polite") and focus management is explicit after modal swaps - File downloads: Use
HX-Redirectto trigger a full browser download
This is a fully functional back-office with real-time and addressable state—without a front-end framework or client router.
---
Where HTMX Shines (Summary)
- Server-rendered truth: No duplicate client validation/view logic
- Speed: Tiny runtime, no hydration tax, multi-transport real-time
- Progressive enhancement by default: Boosted links/forms work even with JS off
- URL + history you can trust: Push state when it matters, refetch instead of snapshot when it's safer
- Composable with small tools: hyperscript/Alpine for local state where needed
Where HTMX Is the Wrong Tool
- Offline-first or heavy client state
- Complex editor UIs that are essentially desktop apps in the browser
- Big component marketplaces where you need SSR + CSR + hydration + a shared component grammar—frameworks still win
---
Migration Playbook (React/Next/Nuxt → HTMX, Carefully)
- Pick one page (e.g., "Orders"). Render it server-side with your existing templates.
- Wrap main in hx-boost, and ensure every link/form inside navigates and swaps into the main target.
- Replace one component at a time with a server partial +
hx-get/hx-post. - Wire history with
hx-push-urlfor bookmarkable filters. - Add SSE for passive updates; use WS only if the client must talk back.
- Keep a framework island where truly necessary (rich editor, whiteboard). Don't force purity.
---
Hotwire/Turbo vs HTMX: Pick Your Flavor
If you live in Rails/Symfony/Django and want HTML-over-the-wire with tight integration, Turbo Drive/Frames/Streams are excellent: persistent navigation, addressable frames, server-pushed streams. HTMX is framework-agnostic and attribute-driven, with SSE/WS extensions that feel equally natural. Both ship production UIs with the same principle: send HTML, not data, and let the server own state.
---
Frequently Tripped Wires
- "My Alpine state resets after a swap." Use the
alpine-morphextension to preserve state, or isolate Alpine state above the swap target. - "Back button shows stale DOM." Disable snapshots (
hx-history="false") and refetch; this aligns with the 2025 guidance. - "How do I detect HTMX requests server-side?" Check
HX-Request: true(andHX-Boosted,HX-Target, etc.). - "How do I patch multiple regions?" Use
hx-swap-oobin the response for each target.
---
The Strategic View
The last decade built client engines to make the web feel like apps. The next decade will make compute fungible again: HTML fragments rendered near data, streamed to a thin, robust client that can also do just enough local logic when necessary. React/Next/Nuxt/SvelteKit are ideal for truly app-shaped problems. But much of your product is still documents that change—forms, tables, lists, details, dialogs. HTMX keeps those honest and fast.
If you're starting a small or medium project in 2025, start with HTMX and add a sprinkle of Alpine or hyperscript. If you're building a large app, make HTMX the default for every page that doesn't demand a client application; reserve the framework for the parts that do.
You will ship faster, debug less, and spend your time where it matters: the server that knows your data.
---
Appendix: Project Size Definitions
These are pragmatic, web-product definitions that map to team reality. They align with known delivery heuristics (stream-aligned teams typically run 5–9 people; delivery health is tracked with DORA's four key metrics).
Small Project
A focused application or module that one small team can ship in a few weeks without inventing a client-side application framework.
Functional scope:- 1–8 addressable screens/routes
- 2–5 core data entities
- 0–2 external integrations
- Interactivity is request/response first; optional SSE; no offline mode
- Target p95 latency: ≤150–200 ms within region
- Traffic: up to ~50 RPS sustained (spikes okay with caching)
- Single region; no multi-tenant complexities beyond basic RBAC
- 1–3 engineers, plus part-time designer/PM
- Delivery horizon: 2–8 weeks
- DORA guardrails: deploy at least daily/weekly; lead time hours→days; failure rate low single-digits; MTTR < 1 day
- Server-rendered HTML + HTMX (
hx-boost, fragments,hx-push-url) - SSE for passive updates; avoid WebSockets unless truly two-way
- One database, one queue at most; no microservices
- You can rewrite any screen in a day
- If the browser reloads, nothing important breaks
- A single on-call can reason about it end-to-end
Medium Project
A product surface with multiple flows and some real-time behavior, still shippable by one stream-aligned team (or two collaborating) without a heavy SPA across the whole site.
Functional scope:- 8–30 addressable screens/routes
- 5–12 core data entities
- 3–8 external integrations (payments, search, auth, analytics, etc.)
- Real-time UX in places (SSE, selective WebSockets). Limited or no offline
- Target p95 latency: ≤200 ms within region across the main flows
- Traffic: ~50–500 RPS sustained
- Multi-tenant basics, richer RBAC, scheduled jobs, background workers
- 3–12 engineers across one or two stream-aligned teams; platform help as needed
- Delivery horizon: 2–6 months in iterations
- DORA guardrails: deploy multiple times per week (or daily), lead time ≤ days, change-failure rate low, MTTR hours
- HTMX for most pages (forms, tables, search, detail, settings)
- SSE broadly; WebSockets where the browser must also talk back (e.g., agent consoles)
- Keep a single SPA "island" only where an in-browser app is justified (rich editor, canvas)
- Clear fragment contract: server returns HTML for fragments on
HX-Request: true, full pages otherwise
- You can ship meaningful features weekly without new frontend build systems
- A new engineer becomes productive inside a week
- Incidents are diagnosable via logs/traces without attaching a JS debugger
---
Appendix: Pointers, Docs, and Deep Dives
- HTMX docs and reference (attributes, headers, config)
- HTMX 2.0 release notes (drop IE, SSE/WS as extensions)
- SSE and WebSocket extensions (install, usage, reconnection)
- History guidance in 2025 ("the fetchening")
- Size on CDN (htmx.min.js.gz ≈16.6 KB)
- Hotwire/Turbo handbook (Drive, Frames, Streams)
- React Server Components, Next.js/SSR (what you take on if you need a client app)
---
If you want to take this further, we can tailor a reference implementation to your stack (Django, Rails, Laravel, Phoenix, Go, Node/Hono) with:
- A fragment/partial convention (HX-Request aware)
- SSE event design + authorization
- WS command schema + queueing
- Edge caching for fragments
- A11y patterns for swaps and modals
That's where the simplicity compounds.
---
About the author: Odd-Arild Meling has built web applications for three decades, from early server-rendered PHP to the full React/SPA era and back again to HTML-first architectures. At Gothar, we use HTMX with Hono on Cloudflare Workers—server-rendered fragments at the edge, sub-50ms TTFB, zero framework overhead. Because most of the web is still documents that change, and the server still knows your data best.