htmx 4: What's New, What Breaks, Why It's Not latest

htmx 4.0.0 shipped today, August 28, 2026. The ajax core now runs on fetch() instead of XMLHttpRequest, attribute inheritance is explicit, the back button issues a real request instead of restoring a cached snapshot, and error responses can be routed by status code. It is a major release that breaks real things, on purpose. And it comes with an unusual instruction: nobody has to move. On npm, 2.x keeps the latest dist-tag until early 2027, 4.0 lives under next, and htmx 2 stays supported indefinitely.
TL;DR
- Every request htmx makes now goes through
fetch();XMLHttpRequestis out, along with the XHR-specific events. - Attribute inheritance is explicit: attributes like
hx-confirmreach descendants only when you add the:inheritedmodifier. - The history DOM cache is gone. The back button re-fetches the page from the server instead of restoring a stored snapshot.
- Error responses swap by default, and the new
hx-statusattribute routes 4xx and 5xx responses by status code to their own targets. - On npm, htmx 2.x keeps the
latestdist-tag until early 2027, and 4.0 lives under thenextdist-tag, so nobody upgrades by accident.
What changed in htmx 4
htmx 4 is a ground-up rework of the library’s internals and a cleanup of its accumulated defaults. Carson Gross, htmx’s creator, announced it in an essay called The fetch()ening in November 2025, together with the first alpha. A string of betas followed through 2026, and the final release landed today.
The big majors I covered this year, Astro 7 and pnpm v12, rewrote their engines precisely so that nothing would change for you: same surface, faster internals. htmx 4 is the other kind of major. Moving to fetch() gave the project a reason to revisit design decisions that had been locked in for years, and the team took it. Implicit inheritance, the history cache, silently ignored error responses: all gone. That is why the release strategy matters as much as the feature list, and I will get to it.
The fetch() rewrite
Every request htmx makes now goes through fetch() rather than XMLHttpRequest. You do not call either API yourself when you write htmx, so on a quiet day you will not notice. The visible consequences are at the edges:
- XHR-specific events are gone.
htmx:xhr:loadstart,htmx:xhr:progress, andhtmx:xhr:aborthave nofetch()equivalent and were removed. - Events follow a new
htmx:phase:actionnaming scheme. If you listen to htmx lifecycle events in JavaScript, every listener needs a rename. - A default request timeout exists now. Requests time out after 60 seconds; in 2.x they could hang forever. The config option moved from
timeouttodefaultTimeout.
The event renames look like this:
| htmx 2.x | htmx 4 |
|---|---|
htmx:beforeRequest |
htmx:before:request |
htmx:afterRequest |
htmx:after:request |
htmx:beforeSwap |
htmx:before:swap |
htmx:afterSwap |
htmx:after:swap |
htmx:configRequest |
htmx:config:request |
htmx:sendError, htmx:swapError, htmx:targetError, htmx:timeout |
htmx:error |
That last row is a real simplification: four error events collapse into one htmx:error.
Extensions changed even more. The announcement puts it plainly: “Switching to fetch() internally let us rethink how extensions can and should work.” The old htmx.defineExtension() API is replaced by htmx.registerExtension(), and the hx-ext attribute is gone entirely: loading an extension’s script is enough. There is a dedicated extension migration guide for authors. The payoff is streaming: new first-party extensions stream HTML into the page over server-sent events (hx-sse), WebSockets (hx-ws), and multipart/mixed responses (hx-multipart).
Attribute inheritance is now explicit
In htmx 2, attributes like hx-confirm and hx-target silently inherited from parent elements. Convenient until it wasn’t: a hx-confirm on a container would attach confirmation dialogs to descendants you forgot about, and hx-disinherit existed purely to fight this. In htmx 4, inheritance only happens when you ask for it with the :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 sits on. hx-inherit and hx-disinherit are both removed, since explicit inheritance makes them pointless. If your codebase leans heavily on the old behavior, setting implicitInheritance to true in the htmx config restores it while you migrate.
Of everything in this release, this is the change I would have asked for. Implicit inheritance was the closest thing htmx had to action at a distance, and debugging it meant walking up the DOM tree in your head.
The back button is a real request now
htmx 2 handled history by snapshotting the DOM into browser storage (localStorage originally, sessionStorage since 2.0.5) and restoring the snapshot on back navigation. That cache is exactly the kind of client-side state that drifts from reality, and InfoWorld’s coverage of the 4.0 line put it bluntly: “It turned out to be brittle.”
htmx 4 drops the cache entirely. Per the announcement, on back navigation htmx re-fetches the page from the server and swaps it into <body>. Your back button now shows what the server says the page is, not what the page happened to look like when you left it. The hx-history attribute and historyCacheSize config are gone with it. If you genuinely need snapshot behavior, an hx-history-cache extension brings it back as an opt-in.
Route error responses with hx-status
htmx 2 refused to swap 400 and 500 responses by default, which meant server-rendered validation errors needed workarounds. htmx 4 flips the default: 4xx and 5xx responses swap like any other (only 204 and 304 are no-swap by default), and a new hx-status attribute routes specific status codes to their own target and swap behavior:
<form hx-post="/signup"
hx-status:422="target:#validation-errors"
hx-status:5xx="target:#server-error"
hx-status:503="swap:none">
Matching goes from specific to general: an exact code like 503 wins over a single-digit wildcard like 50x, which wins over a range wildcard like 5xx. A 422 with the form’s field errors rendered server-side can now land in the right spot with one attribute, which is what hypermedia apps do all day. If you depended on the old ignore-errors behavior, setting the noSwap config to [204, 304, '4xx', '5xx'] restores it.
What else is new in htmx 4
The what’s new page lists more than I can cover, but a few stand out. Morph swaps are built in: innerMorph and outerMorph use the idiomorph algorithm and need no extension. New swap styles textContent and delete join the existing set. A new <hx-partial> element lets one response update several targets, with each partial carrying its own target and swap. View Transitions support is there but disabled by default; flip the transitions config option to turn it on.
The one that caught my eye: a new hx-query attribute issues an HTTP QUERY request, the safe-reads-with-a-body method I wrote about in July after RFC 10008 landed. Watching a brand-new HTTP verb show up as a first-class attribute in a hypermedia library within months is the spec pipeline working the way it should.
The breaking changes in htmx 4
The migration guide is thorough, and the headline breaking changes are the three above: explicit inheritance, error responses swapping by default, and server round-trip history. Beyond those:
- Two attributes trade names, and order matters.
hx-disablebecomeshx-ignore, andhx-disabled-eltbecomeshx-disable. The old name gets reused, so the guide says to renamehx-disabletohx-ignorefirst or you will migrate one attribute into the other. - Removed attributes:
hx-ext,hx-inherit,hx-disinherit,hx-history,hx-vars(usehx-valswith thejs:prefix),hx-params(use thehtmx:config:requestevent),hx-request, andhx-prompt(now an extension). - Config options were renamed or culled.
timeoutisdefaultTimeout,defaultSwapStyleisdefaultSwap,globalViewTransitionsistransitions. A long list of options is simply gone, includinghistoryCacheSize,defaultSwapDelay, and the settling-class options. - Validation events are removed.
htmx:validation:validate,failed, andhaltedare out; native browser validation is the path forward. - Every event listener needs the new names. See the table above.
htmx ships a scanner that finds most of this for you:
npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root
It walks your templates and prints deprecated patterns with file and line numbers. Run it before deciding how big your migration actually is; for a small app the answer may be an afternoon.
Why 4.0 is not latest on npm
Here is the part I find genuinely rare. As of today, the htmx.org package has latest pointing at 2.0.10 and 4.0.0 published under next. That is deliberate, and the announcement spells out why:
we are not marking 4.0 as
latestin NPM because we do not want to force-upgrade users who are relying on non-versioned CDN URLs for htmx. Instead, 2.x will remainlatestand the 4.0 line will remainnextuntil some point in early 2027.
Plenty of production sites load htmx from a CDN URL that resolves whatever latest is. Flipping the tag on release day would silently ship breaking changes to all of them. So the tag waits several more months while 4.0 is, by every other measure, released and stable today.
The announcement frames the whole release this way: “we have made explicit choices that we feel will put htmx-based applications in a good spot for being 100-year web services.” You can roll your eyes at the century talk, but the mechanics back it up. htmx 2 “will continue to be supported indefinitely so don’t feel any pressure to upgrade,” per the same announcement. Compare that with the upgrade treadmill most of the ecosystem runs on, where staying one major behind makes you a straggler within months. htmx is telling you the old version is fine, forever, and meaning it.
Should you upgrade to htmx 4?
For a new project, start on 4. The defaults are better: explicit inheritance, error responses you can actually handle in hypermedia, history that cannot go stale. Install it with npm install htmx.org@next or pin the 4.0.0 CDN URL, since latest will hand you 2.x until early 2027.
For an existing app, there is no clock. Run the upgrade-check scanner to size the work, and if the report is short, do the migration in an isolated git worktree and shake out the event renames. If the report is long, htmx 2 remains supported and maintained, and the team has said so in writing.
The technical changes are good. The release discipline is the story I will remember: a project that shipped a breaking major and then deliberately kept it off the latest tag so nobody gets upgraded by accident. That is what taking backwards compatibility seriously looks like.