The rare major that deliberately breaks things: htmx 4
htmx 4.0.0 shipped on August 28, 2026, and it does something rare for a small, beloved library: it rewrote its core, broke a bunch of long-standing behavior on purpose, and then pointedly refused to make you do anything about it. On npm, 2.x keeps the latest dist-tag until early 2027, 4.0 sits under next, and 2 stays supported indefinitely. You will not be upgraded by accident.
That is the story worth paying attention to, more than any single feature. Most big majors this year did the opposite. Astro 7 and pnpm v12 both swapped out their engines with the explicit goal of changing nothing for you: same surface, faster internals. htmx 4 is the other kind. Moving its ajax layer to fetch() gave the team an excuse to revisit design calls that had been frozen for years, and they took it. Implicit inheritance, a client-side history cache, silently swallowed error responses. All of it is gone.
The fetch() rewrite
Every request htmx makes now goes through fetch() instead of XMLHttpRequest. You never call either API yourself when writing htmx, so on a quiet day you will not notice the swap. The consequences live at the edges.
The XHR-specific events are gone. htmx:xhr:loadstart, htmx:xhr:progress, and htmx:xhr:abort have no fetch equivalent, so they were removed. The rest of the lifecycle events moved to a new htmx:phase:action naming scheme, which means every listener you wired up in plain JavaScript needs a rename:
| htmx 2.x | htmx 4 |
|---|---|
htmx:beforeRequest | htmx:before:request |
htmx:afterSwap | htmx:after:swap |
htmx:configRequest | htmx:config:request |
htmx:sendError, htmx:swapError, htmx:timeout | htmx:error |
There is also a new default request timeout: sixty seconds. In 2.x a hung request could hang forever. The config knob moved from timeout to defaultTimeout while it was at it.
Attribute inheritance is now opt-in
This is the one that will actually bite you if your markup leaned on the old convenience. In htmx 2, attributes like hx-confirm and hx-target silently inherited down to descendants. Handy until a hx-confirm sitting on a container started attaching confirmation dialogs to buttons you had forgotten were nested inside it. The hx-inherit and hx-disinherit attributes existed mostly to fight that behavior.
In 4, an attribute only reaches descendants if you say so with an :inherited modifier:
<div hx-confirm:inherited="Are you sure?">
<button hx-delete="/account">Delete my account</button>
</div>Without the modifier, the attribute applies only to the element it is written on. Both hx-inherit and hx-disinherit are removed, since explicit inheritance makes them pointless. If a codebase depends on the old implicit behavior, there is an escape hatch: set implicitInheritance to true in the htmx config and migrate at your own pace. I'm in favor of the change; the implicit case was always the source of "why is this dialog popping up here" bugs.
The back button makes a real request
htmx 2 kept a DOM cache of visited pages in history, so hitting back restored a stored snapshot without touching the server. That cache is gone in 4. The back button now re-fetches the page from the server.
For most apps this is a non-issue, and arguably more correct: you always come back to current server state instead of a stale copy. But if you built flows that relied on the cheap snapshot restore, or you have pages that are expensive to re-render, this changes the feel. It is the sort of thing you only discover when you start poking at navigation in a staging environment, which is exactly why the release strategy below matters.
Errors swap, and hx-status routes by code
In 2.x an error response was often just ignored, which quietly produced confusing half-updates. In 4, error responses swap into the target by default, and a new hx-status attribute lets you route 4xx and 5xx responses by status code to their own targets. You can finally show a real error message in a real place instead of shrugging at a dead request.
Extensions got a new contract
The move to fetch also reshaped how extensions work. The old htmx.defineExtension() API is replaced by htmx.registerExtension(), and the hx-ext attribute is gone entirely: just load the extension's script and it registers itself. There is a dedicated migration guide for extension authors. The payoff is streaming, with first-party extensions that stream HTML into the page over server-sent events, WebSockets, and multipart/mixed responses.
The trick: it will not be latest
Here is the part I find genuinely interesting. A library this popular usually slaps the big number on latest and lets you break yourself. htmx 4 does not. 2.x keeps latest until early 2027, 4.0 lives under next, and 2 is supported indefinitely. If you run npm i htmx today, you still get 2.x. You have to deliberately point at 4 to get it.
Carson Gross announced the work in November 2025 in an essay called "The fetch()ening," shipped the first alpha with it, and pushed through betas over 2026 before the final landed in August. The whole arc reads as: here is a breaking change, and you do not have to take it this week, or this month, or even this year.
So do you move?
Only if you are touched by the specific things above. Grep for the old event names, for hx-inherit and hx-disinherit, and for any JavaScript that assumes the history cache. If your admin panel or CRUD app is mostly hx-get and hx-post with no custom event listeners, 4 is a near-invisible upgrade that happens to give you a timeout and better error handling for free. If you have built real integrations on top of the lifecycle events or the history model, budget an afternoon. The explicit-inheritance change is the one I would flag to the team first, because it changes what your existing markup does without any code erroring out, which is the sneakiest kind of break there is.
Comments