· 9 min read

htmx 4.0 Shipped and the Team Refused to Make It the npm Default. That Decision Is Your Upgrade Plan.

htmx 4.0.0 was released on August 28, and the htmx team did something unusual with it: they did not mark it as latest on npm. Version 2.x keeps that tag until some point in early 2027, and 4.0 sits on next. The stated reason is that a lot of people link htmx from unversioned CDN URLs, and force-upgrading those sites would be rude.

That single packaging decision is more useful to you than any of the release notes, because it tells you the upgrade is optional and unhurried. htmx 2 will be supported indefinitely. Nothing about your shipping site breaks this week. So the question is not "should I upgrade," it is "is there anything in here I need to know about before I upgrade."

There is exactly one, and it is the CSRF thing.

Attribute inheritance flipped, and that is the whole migration

In htmx 2, many attributes were inherited by child elements by default. You put hx-confirm on a wrapper div and every button inside it inherited the confirmation. This came from the intercooler.js days and was modeled on CSS, and it worked out roughly the way CSS works out: powerful, and occasionally impossible to reason about.

htmx 4 inverts it. Attributes are not inherited unless you say so with an :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>

The htmx team calls this the largest upgrade burden, and they are right, but not for the reason you would guess. A missing hx-confirm is a confirmation dialog that stops appearing. Annoying, obvious in testing, fixed in a minute.

The one that actually hurts is hx-headers. If you put your CSRF token in an hx-headers attribute on a parent element, which is an extremely common pattern, that header stops reaching child elements in htmx 4. Your requests go out without the token. Your server rejects them. The upgrade checker calls this out specifically:

templates/index.html:1: [inheritance] hx-headers needs :inherited suffix
  (descendant on line 3 has hx-delete) (this looks like a CSRF token; without
  :inherited the header does not reach child elements and the server rejects
  the request)

I appreciate that the tool bothers to explain the consequence rather than just flagging the attribute. That is a small thing and most upgrade tools do not do it.

Run the checker even if you are not upgrading

This is my actual recommendation, and it takes about thirty seconds:

npx htmx.org@4.0.0 upgrade-check -- ./templates

It scans .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb, and .hbs by default, and you can add more with --ext. It flags four categories: attributes needing :inherited, renamed attributes, removed attributes, and old event names in hx-on attributes and in your JavaScript where it can find them.

Run it now, read the output, and then decide. If it comes back with three inheritance warnings on hx-target and nothing touching headers, you can close the terminal and upgrade whenever you feel like it. If it comes back pointing at your CSRF setup, you now know something about your codebase that is worth knowing regardless of which htmx version you are on, because it means your security-relevant header is being delivered by an implicit mechanism you were not thinking about.

What is gone, and what replaces it

The removals are small but specific. hx-vars is out, replaced by hx-vals with a js: prefix. hx-prompt is out of core and moved to an extension that keeps the same syntax. hx-disable is renamed to hx-ignore, and hx-disable now means something different: disable during request. That rename is the kind of thing that produces a confusing bug six months later if you skim the changelog. htmx.addClass() is gone in favor of element.classList.add(), which was always the better call.

Event names got standardized to htmx:phase:action[:sub-action]. So htmx:beforeRequest becomes htmx:before:request, htmx:afterSwap becomes htmx:after:swap, and so on. Most error events collapse into a single htmx:error, with HTTP error responses firing htmx:response:error. The htmx:xhr:* events are gone because htmx 4 uses fetch() internally now, and htmx:validation:* events are gone in favor of native browser form validation.

History changed too. htmx 2 snapshotted pages into localStorage for back-button restoration, which broke constantly when third-party JavaScript had mutated the DOM: the mutations got restored but the JavaScript state behind them did not. htmx 4 just re-fetches the page on back navigation and swaps it into <body>, or into [hx-history-elt] if you have one. If you want the old behavior there is an hx-history-cache extension that uses sessionStorage and is built to play nicely with Alpine.js.

The parts that are actually new

Two things I would call genuinely new rather than reorganized. Morphing swaps are built in now, using an improved version of idiomorph. And there is an <hx-partial> tag, which is a clearer alternative to out-of-band swaps when you want to do several targeted updates from one response:

<hx-partial hx-target="#messages" hx-swap="beforeend">
    <div>New message</div>
</hx-partial>
<hx-partial hx-target="#count">
    <span>5</span>
</hx-partial>

The move to fetch() also unlocked streaming, which is where most of the interesting extension work went: hx-sse over text/event-stream, hx-ws over WebSockets, and hx-multipart over multipart/mixed. There is also hx-live, a small front-end scripting layer the team wrote themselves, inspired by Alpine, jQuery and hyperscript. If you do not want to pick extensions, there is an htmax.js bundle that packages htmx with the popular ones.

Where I might be wrong about this

I am telling you to treat this as low urgency, and there is a case against that. If you are starting something new this week, starting on 4.0 is clearly correct and there is no migration to think about. And "htmx 2 is supported indefinitely" is a promise from a small team, not a contract. Big Sky Software has been reliable about this, but indefinite support from a handful of maintainers is a different risk profile than indefinite support from a foundation.

The other thing I could be wrong about is the streaming extensions. If you have been building server-sent events or WebSocket updates onto htmx 2 with your own glue code, the 4.0 extensions might delete a meaningful chunk of that, and that is a real reason to move sooner than "whenever."

What I would actually do

Run the upgrade checker today, on every project using htmx, and read the output. That is the whole action item. If it flags hx-headers, go look at how your CSRF token gets to your requests and make sure you understand it. Then leave htmx 2 alone until you have a reason.

Start new projects on 4.0. Migrate existing ones when you are already in that part of the codebase for another reason, not as a standalone task. The htmx team went out of their way to make sure you are not forced to move, which is a courtesy worth accepting rather than overriding out of a vague sense that newer is better.

Author

Sources

Stay in the Loop

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

Newsletter coming soon. Set PUBLIC_CONVERTKIT_FORM_ID in .env to activate.

Related Posts