12k
All articles

htmx 4.0 Is Here

htmx 4.0 changes inheritance, error swapping, history, and events, with migration tips and rollback options for htmx 2 apps.

OpenReplay Team
OpenReplay Team
htmx 4.0 Is Here

htmx 4.0.0 was released on 28 August 2026. It changes several long-standing defaults: attribute inheritance is now opt-in, error responses swap into the DOM, and the history snapshot cache is gone.

If you maintain an htmx 2 app, the practical question is whether the hx-confirm you hoisted onto a container two years ago still guards anything after the upgrade. It doesn’t, unless you add a modifier. This piece covers what breaks, what reverses each break, and why the npm release strategy means you probably don’t have to act this week. For what htmx is and why hypermedia, the htmx 2.0 walkthrough covers the ground this article starts from.

Key Takeaways

  • htmx 4 makes attribute inheritance explicit via the :inherited modifier, and setting htmx.config.implicitInheritance to true restores htmx 2 behaviour as a migration bridge.
  • Only 204 and 304 responses skip the swap by default in htmx 4, so a server-rendered 422 now lands in the target instead of being discarded; htmx.config.noSwap = [204, 304, '4xx', '5xx'] reverts it.
  • Event names follow an htmx:phase:action pattern, which has no config key: every htmx listener in your JavaScript needs renaming, or you install the htmx-2-compat extension.
  • Rename hx-disable to hx-ignore before upgrading, because htmx 4 reassigns the name hx-disable to the job hx-disabled-elt used to do.
  • htmx 2.x holds the npm latest tag while 4.0 sits under next, so unversioned CDN URLs are not force-upgraded, and the announcement states htmx 2 will be supported indefinitely.

What Changed in htmx 4?

htmx 4 moves the library’s request internals from XMLHttpRequest to fetch(), and that rewrite is what made the rest of the release possible. Swapping the transport was already a breaking change, so the team used the same major to reset defaults that had accumulated since htmx 1.

You don’t call either API directly when writing htmx, so the transport swap itself is invisible in your templates. The consequences show up at the edges: the XHR-specific lifecycle events have no fetch() equivalent and were removed, and htmx 4 sets htmx.config.defaultTimeout to 60000, where htmx 2 let a request hang indefinitely.

On the version number: htmx creator Carson Gross had said there would never be an htmx 3, so the release skips straight to 4.0 and the promise survives on a technicality. He set out the reasoning in the essay that announced the rewrite in November 2025.

Attribute Inheritance Is Now Explicit

Inheritance in htmx 4 happens only when you ask for it. An attribute on a container applies to that container alone unless you add the :inherited modifier, and the modifier works on any attribute: hx-boost:inherited, hx-target:inherited, hx-confirm:inherited.

<!-- htmx 4: the confirm reaches both buttons -->
<div hx-confirm:inherited="Are you sure?">
  <button hx-delete="/account">Delete My Account</button>
  <button hx-put="/account">Update My Account</button>
</div>

A value on a child wins over the inherited one by default. Use :append when you want the two combined instead, which is the composition case that trips people up:

<div hx-vals:inherited="tenant:acme">
  <button hx-post="/save" hx-vals:append="source:save-btn">Save</button>
</div>

Without :append, the button’s own hx-vals takes the place of the inherited one and tenant never reaches the server. Where no ancestor sets the attribute at all, the appended value is the only value sent. Names vary a little by attribute: the hx-disable reference page documents :merge for the same job of adding to a parent’s value.

hx-inherit and hx-disinherit are removed, since explicit opt-in makes both unnecessary. If your templates lean on the old behaviour, set htmx.config.implicitInheritance to true to restore it while you migrate. Treat that as a bridge, not a destination.

Error Responses Swap by Default

In htmx 4 a response reaches the target whatever its status code, with only 204 and 304 held back. A server-rendered 422 validation page now lands in the target instead of being silently dropped, which is what hypermedia apps wanted all along. An HTTP error response also fires an htmx:response:error event.

The new hx-status attribute routes individual codes to their own target and swap:

<form hx-post="/submit"
      hx-target="#result"
      hx-status:422="target:#validation-errors"
      hx-status:5xx="target:#server-error"
      hx-status:503="swap:none">
  <input name="email">
  <button type="submit">Submit</button>
</form>

htmx tries the most specific pattern first: the exact code, then a pattern with the last digit masked such as 50x, then one with the last two masked such as 5xx. Inside the attribute value you can set swap:, target:, select:, push:, replace: and transition:.

If your backend returns error pages that were never designed to be swapped, set htmx.config.noSwap to [204, 304, '4xx', '5xx'] and you have htmx 2’s behaviour back.

Back Navigation Is a Real Request Now

htmx 4 drops the client-side DOM snapshot cache that backed history in htmx 2. Press back and htmx asks the server for the page again, then swaps what comes back into <body>, or into an [hx-history-elt] element where the page has one.

The practical effect is that the back button shows what the server currently says the page is, not a snapshot frozen at the moment you navigated away. That removes a whole class of bug where third-party scripts mutated the DOM and the restored snapshot replayed those mutations into a broken state. It also means back navigation costs a request.

The hx-history attribute is gone with the cache. If you need snapshots, the hx-history-cache core extension reintroduces them as an opt-in.

Event Names Follow the htmx:phase:action Pattern

Every htmx lifecycle event was renamed to an htmx:phase:action[:sub-action] shape. The announcement gives htmx:beforeRequest as htmx:before:request and htmx:beforeSwap as htmx:before:swap; htmx:afterSwap becomes htmx:after:swap.

This is the one change with no configuration escape hatch. Every listener needs editing:

// htmx 2
document.body.addEventListener('htmx:afterSwap', (e) => {
  initTooltips(e.detail.target);
});

// htmx 4
document.body.addEventListener('htmx:after:swap', (e) => {
  initTooltips(e.detail.target);
});

Most error events are consolidated into htmx:error, with HTTP error responses firing htmx:response:error. The XHR-specific events are simply gone, since fetch() exposes no equivalent. If hand-editing listeners is the bulk of your migration, the htmx-2-compat extension maps the old event names onto the new ones and also restores implicit inheritance and hx-ext.

What’s New in htmx 4 Rather Than Broken?

Three htmx 4 additions are worth the upgrade on their own: morph swaps, the <hx-partial> element, and the rewritten streaming extensions. Morph swaps are in core, so state-preserving DOM updates no longer need an extension. The <hx-partial> element lets one response update several targets, each carrying its own target and swap:

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

Because the target and the swap style sit on the partial itself, the response states what it wants done with each piece, rather than leaving you to work it out from hx-swap-oob attributes scattered through the markup. Note that out-of-band ordering reversed in htmx 4: the main content swaps first.

The streaming extensions are the other headline. Both the SSE and WebSocket extensions were rebuilt for this release, and a fresh set ships alongside them: hx-multipart, hx-live, hx-targets, hx-ptag, hx-csp, hx-download, hx-prompt and hx-history-cache. Connection attributes are namespaced, so SSE connects with hx-sse:connect and WebSockets with hx-ws:connect.

Upgrading to htmx 4, and Why There’s No Rush

Upgrading to htmx 4 starts with the scanner: run it before you plan anything. npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root walks your project and prints each deprecated pattern with its file and line number, which is enough to size the job in an afternoon.

npx htmx.org@4.0.0 upgrade-check -- ./templates
npx htmx.org@4.0.0 upgrade-check --ext .vue ./path/to/project/root

Out of the box it looks at .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb and .hbs. Single-file component formats are not in that set, so .vue, .svelte, .jsx and .astro templates go unchecked unless you pass --ext.

Do one rename before you touch anything else: hx-disable becomes hx-ignore, and hx-disabled-elt becomes hx-disable. The old name is reused for a different job, so if you migrate hx-disabled-elt first you will overwrite attributes that still mean the htmx 2 thing.

Changehtmx 4 defaultWhat restores htmx 2
Attribute inheritanceExplicit, via :inheritedhtmx.config.implicitInheritance = true
Error response swappingOnly 204/304 skip the swaphtmx.config.noSwap = [204, 304, '4xx', '5xx']
HistoryServer re-fetch on backhx-history-cache extension
Event nameshtmx:phase:actionNo config key; htmx-2-compat extension

Then the part that decides whether any of this is urgent: on npm, htmx 2.x holds the latest dist-tag and 4.0.0 is published under next. The announcement is explicit that this is deliberate, so that sites loading htmx from an unversioned CDN URL are not force-upgraded into breaking changes, with 2.x staying latest until early 2027. 2.x remains supported indefinitely.

That maps to four positions. An unversioned CDN URL keeps serving 2.x until the tag flips, which is the only case with a future deadline attached. A pinned CDN URL and an exact npm pin never change on their own. An npm range like ^2.0.0 stays inside 2.x regardless of dist-tags. To install 4.0 today, pin it: npm install htmx.org@4.0.0, or use the versioned CDN path.

Start new projects on 4.0. For an existing htmx 2 app, run the scanner, do the hx-disable rename first, and decide from the report length whether to migrate now or to revisit it before the dist-tag moves.

FAQs

How do I load an htmx extension in htmx 4 now that hx-ext is removed?

Include the extension script after the htmx script and its attributes work immediately, with no activating attribute needed. Load dist/ext/hx-sse.js alongside htmx.min.js and you can use hx-sse:connect directly. The htmax.js distribution ships htmx pre-bundled with the most popular extensions in a single file, with those attributes automatically available. Extension authors register through htmx.registerExtension with a name and a method map.

Can I avoid the extra server request that htmx 4 makes on back navigation?

Yes. The hx-history-cache core extension restores history from sessionStorage rather than issuing a full server request, which is the closest equivalent to htmx 2 snapshots. Two configuration values change the behaviour instead: htmx.config.history set to 'reload' does a full page reload on history navigation, and htmx.config.history set to false disables history handling. The htmx 2 localStorage snapshot cache is gone.

What replaces hx-vars and hx-prompt in htmx 4?

hx-vars is removed, and computed values move to hx-vals with the js: prefix. hx-prompt is removed from core and ships as an extension: load the hx-prompt extension to keep the same syntax. Other removed attributes include hx-ext, hx-inherit, hx-disinherit and hx-history. hx-disabled-elt is renamed rather than removed: it becomes hx-disable, and the old hx-disable becomes hx-ignore, as the rename table in [What's New in htmx 4](https://four.htmx.org/docs/whats-new-in-htmx-4) sets out. The upgrade-check scanner marks these two as renamed-attr and the genuine removals as removed-attr, each with the file, the line number and the suggested replacement.

Does hx-swap-oob still work in htmx 4, and when should I use hx-partial instead?

hx-swap-oob still works, but htmx 4 flips the order: the main content goes in first, and out-of-band and hx-partial elements follow it in document order. Reach for hx-swap-oob when you are swapping one element for an updated copy of the same element, and for hx-partial when a single response has to update several places, since each partial states its own hx-target and hx-swap rather than relying on attributes spread through the markup.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.