DEV Community

jamilxt
jamilxt

Posted on

htmx 4.0 Just Shipped: What Changed, What Breaks, and How to Migrate This Weekend

On Friday the htmx team shipped a brand new major version, and then did something almost no library does: they told nobody to upgrade. htmx 4.0.0 landed on August 28 after eight months of work, and on npm the 2.x line keeps the latest tag until early 2027. The 4.0 line sits under next so that sites pulling htmx from an unversioned CDN URL do not get breaking changes shipped into production by accident. Meanwhile, the announcement says htmx 2 "will continue to be supported indefinitely."

I have used htmx on side projects for years, mostly for admin panels and dashboards where a full SPA framework felt like paying a mortgage on a tool shed. I have not yet migrated a production app to 4.0, so treat this as a well-researched migration plan, not a war story. But I spent the weekend reading the release notes line by line, and the changes are more interesting than the usual major-version churn. Some of them will silently break real apps. One of them can break your CSRF protection. Here is what actually changed, what it means for your code, and the checklist I would run before touching anything.

The core rewrite: XMLHttpRequest is gone

The biggest change is invisible until it is not. Every request htmx makes now goes through fetch() instead of XMLHttpRequest. The team had kept XHR for backwards compatibility going back to the intercooler.js days, and the rewrite happened almost by accident: one of the maintainers built the minimal fixi library on fetch(), liked it, and the port grew from there.

For most code, nothing changes. You write the same hx-get and hx-post attributes as before. The consequences live at the edges:

  • XHR-specific events are removed. htmx:xhr:loadstart, htmx:xhr:progress, and htmx:xhr:abort have no fetch() equivalent. If you were using htmx:xhr:progress for upload progress bars, that pattern is gone and needs a new approach.
  • Every lifecycle event got renamed. The old names had grown organically over a decade. htmx 4 standardizes on a htmx:phase:action pattern. htmx:beforeRequest becomes htmx:before:request, htmx:afterSwap becomes htmx:after:swap, htmx:configRequest becomes htmx:config:request. If you listen to htmx events anywhere in JavaScript or in hx-on attributes, every one of those listeners needs a rename.
  • Error handling collapsed. Most error events merge into a single htmx:error, and HTTP error responses fire htmx:response:error. Validation events are removed in favor of native browser form validation.
  • A default timeout exists now. Requests time out after 60 seconds. In htmx 2 they could hang forever. If you have a legitimately long-running endpoint, you need to raise the new defaultTimeout config value, because in 2.x the equivalent timeout defaulted to zero, meaning no timeout at all.

That last one is the kind of change that will not show up in testing on a fast connection and then will show up as a confusing production failure on slow networks. Worth checking before you ship.

The biggest upgrade trap: inheritance is now explicit

In htmx 2, many attributes were inherited by default. Put hx-confirm="Are you sure?" on a parent div and every htmx button inside it picked up that confirmation. This came from intercooler.js, was inspired by CSS, and worked about as well as CSS inheritance usually does: powerful, and occasionally impossible to figure out.

htmx 4 flips the default. Attributes are not inherited unless you explicitly mark them with an :inherited suffix:

<!-- htmx 2: both buttons confirm -->
<div hx-confirm="Are you sure?">
  <button hx-delete="/item/1">Delete</button>
</div>

<!-- htmx 4: only inherited when you say so -->
<div hx-confirm:inherited="Are you sure?">
  <button hx-delete="/item/1">Delete</button>
</div>
Enter fullscreen mode Exit fullscreen mode

Here is why this is the change I would lose sleep over. The release's own upgrade checker flags this exact case in its example output: an hx-headers attribute on a parent element carrying what looks like a CSRF token down to child elements making hx-delete calls. Under htmx 4 without :inherited, that header simply does not reach the child request, and the server starts rejecting deletes with a 403. Nothing in the browser looks broken. The button renders, the request fires, the server says no.

If your app relies on inherited CSRF headers, and a lot of htmx apps do exactly this, migrating naively breaks security-critical behavior in a way that looks like a server bug. The upgrade checker specifically detects this pattern and warns about it, which is one more reason to run it before anything else.

There is also a name-swap trap. hx-disable becomes hx-ignore, and hx-disabled-elt becomes hx-disable. The old name gets reused with a new meaning, so the migration guide says to rename hx-disable to hx-ignore first, then rename hx-disabled-elt to hx-disable. Do it in the wrong order and you migrate one attribute into the other.

The back button is a real request now

htmx 2 kept a history cache in localStorage, snapshotting your DOM so back-navigation could restore it instantly. It sounded great and caused endless support headaches, because those snapshots froze mutations made by third-party JavaScript. On restore, the mutated DOM came back, but the JavaScript that created those mutations did not re-run. Everyone has seen some version of this bug: a widget works on fresh load and is zombie-broken after navigating back.

htmx 4 drops the local cache. On back navigation, htmx re-fetches the page from the server and swaps it in. Third-party scripts mostly just work now, and with reasonable HTTP caching the round trip is fast. If you genuinely need local-cached history, there is a new hx-history-cache extension that restores from sessionStorage and is designed to coexist with Alpine.js.

My take: re-fetching is the more boring and more correct behavior. I have debugged exactly one of those zombie-DOM bugs and it cost me an evening I do not want back.

The new features actually worth the move

Two headline features and a pile of extensions make 4.0 more than a cleanup release.

  • Morph swaps, built in. The idiomorph algorithm, which preserves DOM nodes and their state instead of tearing everything down, is now integrated natively. If you have ever swapped in fresh HTML and watched an input lose focus or a video element restart, morphing fixes that class of problem.
  • The <hx-partial> tag. Out-of-band swaps in htmx 2 worked but read like a hack. The new tag lets one response update multiple targets cleanly:
<hx-partial hx-target="#messages" hx-swap="beforeend">
  <div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
  <span>5</span>
</hx-partial>
Enter fullscreen mode Exit fullscreen mode
  • A rebuilt extension system. The fetch() migration let the team rethink extensions. New ones include hx-preload (fetch on hover to kill perceived latency), hx-download (native file downloads), hx-alpine-compat, and three streaming options: hx-sse for server-sent events, hx-ws for WebSockets, and hx-multipart for multipart streams.
  • hx-live, a small scripting language. The team shipped their own Alpine-inspired scripting extension with what they call DOM-based, HATEOAS-friendly reactivity. I have not tried it yet, so no verdict, but it signals where the project is heading: a fuller hypermedia-first stack, not just request-and-swap.
  • htmax.js, an opinionated bundle. If picking extensions sounds like work, the distribution ships a bundle combining htmx with the most popular ones in a single file.

Your migration plan, in order

Do not freehand this migration. The team shipped a command-line checker and an official agent skill for AI coding assistants, which tells you exactly how they expect 2026 migrations to happen. Here is the sequence I would run:

  1. Run the upgrade checker first. npx htmx.org@4.0.0 upgrade-check -- ./templates scans your templates and JavaScript, and flags inheritance issues, renamed attributes, removed attributes, and old event names. Add --ext .vue --ext .svelte if you have those file types.
  2. Fix the attribute name swap before anything else. Rename hx-disable to hx-ignore, then rename hx-disabled-elt to hx-disable. Order matters, because the target name is reused.
  3. Audit every inherited attribute. Pay special attention to hx-headers carrying CSRF tokens, and to hx-confirm and hx-target on parent elements. Add :inherited where behavior must stay.
  4. Rename all event listeners. Search your codebase for htmx: and update to the new colon-delimited names, in both JavaScript and hx-on attributes.
  5. Replace removed attributes. hx-vars becomes hx-vals with a js: prefix, hx-params logic moves to the htmx:config:request event, and hx-prompt needs its extension loaded.
  6. Check the 60-second timeout. Any endpoint that legitimately runs longer needs defaultTimeout raised.
  7. Test error handling. Error responses now swap into the DOM by default. If your server returns partial HTML errors, verify they render sensibly.
  8. Test back-button behavior. Any code depending on the localStorage history cache needs a look.

One more upgrade lever worth knowing about: htmx 4 ships an htmx-2-compat option to ease the transition, and the checker supports common template extensions out of the box, including .html, .php, .erb, and Jinja2.

The release discipline is the real story

Step back from the feature list, because the most instructive part of this release is how it shipped. A project released a breaking major version and deliberately kept it off the latest npm tag for months, precisely because they knew thousands of sites load htmx from unversioned CDN URLs and would have been force-upgraded with no warning. The announcement frames the design goals around building what they call 100-year web services, and whether or not you buy the century talk, the mechanics back it up: old version supported indefinitely, no forced upgrades, a checker that catches the silent breakages, and upgrade tooling built for AI assistants because that is how code gets migrated now.

Compare that with the news cycle from this same week, where an AI lab terminated a code editor's model access on ten weeks of notice because of a corporate acquisition. One ecosystem treats stability as a promise. The other treats your dependencies as leverage. When I pick tools for my own infrastructure, that contrast is the whole decision.

What I would actually do: if you are on htmx 2 and happy, stay there, exactly as the team suggests. If you are starting something new, start on 4.0, because explicit inheritance and morph swaps are simply better defaults. If you maintain an existing htmx app, run the checker this week even if you do not migrate, because its report is a free audit of every place inheritance and event names could surprise you later.


I write about web development, backend engineering, and AI infrastructure every week. Subscribe, it's free.

Have you built anything with htmx, or are you Team React all the way down? And if you have already migrated an app to 4.0, what broke that the release notes did not warn you about?

Top comments (0)