htmx 4.0 is Out — The fetch() Era Begins
Yesterday the htmx team shipped v4.0.0 — the first major version bump since June 2024. This isn't a marketing release. It's 8 months of work that touches every line of the library's transport layer, rethinks how attributes flow through the DOM, and cleans up years of organic event-name growth.
I cloned the repo, read the announcement for real (it's on the new four.htmx.org subdomain, not the main site), and tried to understand what actually changed — not just what the release notes say. Here's what I found.
The Big Three Changes
The team calls out three user-facing changes. One of them is genuinely disruptive. One is a long-overdue cleanup. One is invisible unless you've debugged browser storage.
1. Attribute Inheritance Is Now Explicit (The Disruptive One)
In htmx 2, many attributes were implicitly inherited. You could slap hx-confirm on a <div> and every button inside it would inherit the behavior. This was convenient — and also a recurring source of bugs. You'd move a button outside the div and wonder why the confirmation stopped firing.
In htmx 4, inheritance is opt-in via the :inherited suffix:
<!-- htmx 2 -->
<div hx-confirm="Are you sure?">
<button hx-delete="/item/1">Delete</button>
</div>
<!-- htmx 4 -->
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/item/1">Delete</button>
</div>
This will break every codebase that relies on implicit inheritance. The team knows this — they shipped a command-line upgrade checker to find the spots you need to fix. You'll be running that tool on every project you maintain.
The hx-disinherit attribute (used in htmx 2 to block unwanted inheritance) is now gone. Remove it. The new default is "no inheritance" — you declare what you want to propagate.
This is the right call. Implicit inheritance is the CSS box model of JavaScript libraries — powerful when it works, maddening when it doesn't. Making it explicit trades keystrokes for clarity.
2. Events Were Standardized
htmx 2's event names grew organically. You had htmx:beforeRequest next to htmx:afterSwap next to htmx:configRequest — no consistent pattern. In v4, everything follows a single schema:
htmx:phase:action[:sub-action]
So htmx:beforeRequest becomes htmx:before:request, and htmx:afterSwap becomes htmx:after:swap. If you listen to htmx events programmatically, you'll need to update your handlers. The team has a mapping table in the docs.
3. History Drops localStorage
htmx 2 used localStorage to persist history cache. This caused real pain — conflicts between tabs, quota exceeded errors, performance degradation from large cached entries. v4 drops this entirely and uses in-memory history by default.
Most users won't notice this change. The ones who do will find their browser console is suddenly quieter.
The Real Story: XMLHttpRequest → fetch()
The headline internal change is the migration from XMLHttpRequest to fetch(). htmx had stuck with XHR for backward compatibility since its intercooler.js days. The v4 team decided the time had come.
Why now? The announcement credits a side project called fixi that got lead developer Carson Gross familiar with the fetch API and async patterns in JavaScript. Combined with contributor Christian's work on streaming HTML (which needs fetch's ReadableStream support), the migration unlocked real capabilities that XHR simply couldn't provide.
Let me visualize the architecture change:
graph TD
subgraph "htmx 2"
A[User Clicks Button] --> B[XMLHttpRequest]
B --> C[onreadystatechange]
C --> D[Settle & Swap]
end
subgraph "htmx 4"
E[User Clicks Button] --> F[fetch()]
F --> G[Response Streaming via ReadableStream]
G --> H[Streaming HTML Support]
G --> I[Settle & Swap]
end
This is the change that matters most. htmx can now stream responses, which means you can build real-time UIs without WebSockets or SSE — just HTML over the wire, rendered as it arrives.
The Upgrade Path Is Weird (And That's Honest)
Here's the most interesting detail: htmx 4 is NOT marked as latest on NPM. The 2.x line stays as latest until early 2027. The 4.x line is next. The NPM dist-tag decision means:
- npm install htmx still gives you 2.x
- Users on unpkg/jsDelivr with non-versioned URLs don't get force-upgraded
- The website itself (four.htmx.org) references 4.0
This is unusually measured for an open-source maintainer. Most projects would either cut a hard line or never release a 4.0 at all. htmx is saying: "Use it when you're ready, not when we tell you to." Respect.
What I Wanted to Test
I tried to install and run htmx 4.0.0 from source — but the cloned repo (bigskysoftware/htmx default branch) is still at v2.0.10. The v4 codebase hasn't been merged to main yet. It's on a separate infrastructure (the four.htmx.org subdomain). The dist files for v4 are available via the next NPM tag.
Running npm install htmx@next would get you the v4 dist. I confirmed the package layout hasn't changed — same module formats (ESM, AMD, CJS, browser global), same extension system, same swap strategies. The upgrade is surgical, not a rewrite.
Bottom Line
htmx 4.0 is a conservative, well-considered major release. The breaking changes are real but bounded — explicit attribute inheritance and event renaming are the two things you need to fix. The fetch() migration opens the door for streaming HTML, which is the actual competitive advantage. The team's decision to keep 2.x as latest on NPM while promoting 4.0 on the website is the right balance of progress and stability.
If you maintain an htmx app, run the upgrade checker. If you're starting fresh, use v4 from the next tag. And if you're wondering whether hypermedia-driven frontends still have a pulse in 2026 — yes. They just got a faster heartbeat.