DEV Community

Vincent Tran
Vincent Tran

Posted on Originally published at 0xgosu.dev on

Htmx 4: A Modern Runtime for Server-Driven HTML

Htmx has always made a deliberately unfashionable proposition: a server can return HTML, the browser can place that HTML into the document, and a useful application can emerge without duplicating the entire interface as client-side state.

Version 4 does not abandon that proposition. A button still needs little more than hx-post, a URL, and a swap rule. What changed is the runtime underneath. The request engine now uses fetch() instead of XMLHttpRequest; inherited behavior must be declared; HTTP errors can be rendered as ordinary HTML; morphing and multi-target responses are first-class; and extensions can consume streams without fighting the core.

That makes htmx 4 an unusual major release. Its public idea remains almost boringly stable while its internal model becomes more native to the modern browser. The result is not a new front-end framework so much as a sharper contract between HTML, HTTP, the DOM, and the server that owns application state.

The central idea is still hypermedia

A conventional client-rendered application often asks an API for data, stores that data in JavaScript, renders components, and then reconciles local state with the server. Htmx removes much of that middle layer. The server responds with the representation the user needs: HTML.

<button
  hx-post="/tasks"
  hx-target="#task-list"
  hx-swap="beforeend"
>
  Add task
</button>

Enter fullscreen mode Exit fullscreen mode

When the button is activated, htmx gathers the request context, calls the server, parses the returned markup, and appends it to #task-list. The server remains responsible for validation, authorization, persistence, and presentation. The browser remains responsible for interaction, navigation, focus, and the document.

This is more than a shorter way to write fetch(). The attributes describe a hypermedia control: which action is available, where it goes, and how its representation should enter the current page. The response can contain links and forms that describe the next valid actions, just like a full document does.

Htmx 4 keeps that surface while reorganizing the lifecycle below it.

“A
The browser sends context and receives a representation. Htmx coordinates the trip, but HTML remains the application protocol.

fetch() changes what the core can become

Htmx previously used XMLHttpRequest because it worked across an older browser landscape and exposed upload progress events that applications depended on. By 2026, that compatibility choice had become architectural debt. The htmx team rebuilt the request path around the promise-based Fetch API after experimenting with a smaller project called Fixi and with streaming HTML.

For an ordinary hx-get, the migration should be invisible. The important difference appears at the edges. Fetch uses standard Request, Response, headers, abort signals, modes, credentials, and streaming bodies. Those primitives make the request lifecycle easier to compose with async code and easier for extensions to intercept.

The new event model reflects that cleaner pipeline. Events now follow a predictable htmx:phase:action[:sub-action] shape:

Htmx 2 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

Every request event also receives a consistent context object. An extension no longer needs a loose collection of event-specific details to understand the source element, request configuration, response, and swap. Request completion has a finally phase, whether the operation succeeds, fails, or is cancelled.

Htmx also removed wrappers for browser functions that now have dependable native equivalents. htmx.addClass() becomes element.classList.add(), htmx.closest() becomes element.closest(), and htmx.remove() becomes element.remove(). This is healthy subtraction. A small library should not permanently carry convenience APIs once the platform has absorbed them.

Explicit inheritance makes locality visible

The largest migration change is not related to Fetch. It is the decision to stop inheriting most htmx attributes implicitly.

In htmx 2, a container could put hx-confirm, hx-target, or hx-headers on a parent, and descendant controls would acquire the behavior. That was concise, much like CSS inheritance. It could also make a button’s behavior impossible to understand by reading the button. A security header or destructive confirmation might come from an ancestor several templates away.

Htmx 4 requires the author to mark that reach:

<section
  hx-confirm:inherited="Delete this item?"
  hx-headers:inherited='{"X-CSRF-Token":"…"}'
>
  <button hx-delete="/items/42">Delete</button>
</section>

Enter fullscreen mode Exit fullscreen mode

The :inherited suffix is not cosmetic. It tells a reviewer that the attribute is deliberately part of the descendants’ behavior. A child can use :append when it needs to extend an inherited selector or value instead of replacing it.

This change moves htmx toward locality of behavior : the closer a declaration is to the element it controls, the less hidden context a reader needs. Shared behavior is still possible, but its scope is advertised at the declaration site.

It also creates a real upgrade hazard. If a CSRF header previously lived on a layout container, the page may render normally after a version bump while child requests begin failing. The official upgrade checker searches templates and scripts for inheritance, old event names, removed attributes, and obsolete APIs. Treat its report as a starting point, then test the actual request paths.

Error pages are now useful fragments

Htmx 2 did not swap 4xx or 5xx responses by default. Htmx 4 swaps every HTTP response except 204 No Content and 304 Not Modified.

That default is a better fit for server-driven interfaces. Validation failure is still an application state, and the server often has the best information for rendering it. A 422 response can contain field errors; a 409 can explain a version conflict; a 500 can replace a panel with a recovery action. An error status should not force the response body to become invisible.

Htmx 4 adds hx-status when different status families need different destinations or swap rules:

<form
  hx-post="/account"
  hx-target="#account-card"
  hx-status:422="target:#errors swap:innerHTML"
  hx-status:5xx="target:#service-status swap:innerHTML"
>
  <!-- fields -->
</form>

Enter fullscreen mode Exit fullscreen mode

The design consequence belongs on the server: error responses must be valid fragments for the target that will receive them. Status codes continue to carry HTTP meaning, while HTML carries the presentation and the next possible action.

One response can update several parts of the page

Server-driven applications frequently need to change more than the clicked element. Adding a message might append to a timeline, update an unread count, and replace a pagination control. Htmx has long supported out-of-band swaps, where specially marked elements in a response replace matching elements elsewhere in the document.

Htmx 4 adds a clearer tool: <hx-partial>. Each partial names its target and swap strategy explicitly.

<article id="new-message">The build finished.</article>

<hx-partial hx-target="#messages" hx-swap="beforeend">
  <article>The build finished.</article>
</hx-partial>

<hx-partial hx-target="#unread-count" hx-swap="innerHTML">
  <span>5</span>
</hx-partial>

Enter fullscreen mode Exit fullscreen mode

The main response swaps first. Partials and out-of-band elements follow in document order. That ordering encourages each update to be independently meaningful instead of relying on a hidden side effect from an earlier DOM mutation.

This is a small but important form of response orchestration. The server can atomically describe the visible consequences of one operation without returning JSON and asking client code to distribute fields among components.

Morphing preserves the parts users are touching

Replacing innerHTML is easy to reason about, but it can destroy local browser state. An input may lose selection, an element may lose focus, a media player may restart, or a custom element may be recreated even though most of its structure did not change.

Htmx 4 includes innerMorph and outerMorph swap modes based on an improved Idiomorph algorithm. Instead of discarding the target subtree, a morphing swap matches old and new nodes and applies the smallest useful set of changes. Stable nodes can keep their identity and therefore retain browser-owned state.

<section
  id="profile"
  hx-get="/profile/edit"
  hx-swap="outerMorph"
>
  <!-- current profile -->
</section>

Enter fullscreen mode Exit fullscreen mode

Morphing is not automatically superior. A simple fragment is often safest to replace wholesale. Morphing earns its complexity when the target contains live controls, custom elements, media, or third-party widgets whose identity matters. Htmx exposes selectors for skipping entire nodes or children during a morph, which helps protect stateful islands.

The broader pattern is useful: let the server own the desired HTML, but let the browser preserve the physical DOM objects that should survive the transition.

Streaming becomes an extension concern

Moving to Fetch gave htmx a better substrate for streamed responses, but the core does not force one streaming protocol on every application. Htmx 4 ships focused extensions for Server-Sent Events, WebSockets, and multipart responses.

The hx-multipart extension can consume multipart/mixed or multipart/parallel. Each part can carry HTML plus its own HX-* action headers. A server can therefore begin a response with an immediate placeholder, stream later sections as work completes, and target each piece without inventing a client-side message bus.

SSE remains a good match for ordered one-way server updates. WebSockets remain useful when both browser and server send messages. Multipart works naturally when one HTTP operation produces several representations. The architecture is modular: all three end at the same swap machinery.

That separation keeps the core small while making the extension boundary more powerful. Htmx 4 extensions register directly and can participate in request, response, and swap phases. They are loaded by including their scripts; hx-ext is gone. Sites can restrict the allowed extension names through configuration when they want an explicit boundary.

HCON gives attributes a small structured language

As attributes gained options, htmx needed a notation less noisy than embedded JSON. HCON—htmx Configuration Object Notation—supports space-separated key-value pairs, flag booleans, numbers, quoted strings, and dotted keys.

<meta
  name="htmx-config"
  content="transitions defaultTimeout:5000 sse.reconnect:true"
>

<button
  hx-get="/report"
  hx-config='credentials:"include" cache:"no-cache"'
>
  Refresh
</button>

Enter fullscreen mode Exit fullscreen mode

JSON remains valid when a server already produces it. HCON is for handwritten markup: compact enough to scan, structured enough to avoid a growing collection of one-off parsers. The same notation appears in triggers, swap modifiers, request configuration, headers, values, and HX-Location.

hx-live handles the client state that remains

Hypermedia does not eliminate every local interaction. Dropdowns open before a request. Character counts change as a user types. Tabs, disclosure widgets, and temporary selections are often browser concerns.

The new hx-live extension provides a small DOM-oriented scripting layer for those cases. It includes a query helper, directional selectors, DOM utilities, async helpers, typed access to attributes and data, and reactive bindings such as :text, :class, and :hidden.

Its important constraint is philosophical: the DOM is the state store. A reactive expression reads nearby element state and updates nearby presentation. Durable application state still belongs on the server and still travels through hypermedia responses.

That makes hx-live a pressure valve rather than an invitation to rebuild a second application inside the browser. Use it for ephemeral interaction that would otherwise require repetitive event-listener code. When state must survive navigation, coordinate users, enforce permissions, or participate in transactions, send it to the server.

History now favors fresh documents over frozen DOM

Htmx 2 cached history snapshots in localStorage. Restoring those snapshots could also restore mutations made by unrelated scripts without restoring the JavaScript runtime state that produced them. The page looked alive but contained a fossilized DOM.

Htmx 4 removes that default cache. Back and forward navigation re-fetches the page and swaps the result into <body> or a designated history element. Correct HTTP caching can make the request cheap while still giving scripts a clean document to initialize.

Applications that truly need local snapshots can load hx-history-cache, which uses sessionStorage and makes the behavior explicit. Again, version 4 chooses an honest boundary: fetching a fresh representation is the default; local reconstruction is an optional capability with a name.

The migration is a behavioral audit

The safest upgrade is not a blind package replacement. It is a short audit of where behavior crosses markup boundaries.

“A
Most pages keep their markup. The work is concentrated at implicit inheritance, event listeners, response policy, history, and extensions.

Start with a pinned version and run the checker:

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

Enter fullscreen mode Exit fullscreen mode

Then review the changes in an order that avoids ambiguous renames:

  1. Rename old hx-disable—which meant “ignore this subtree”—to hx-ignore.
  2. Rename hx-disabled-elt to the new hx-disable.
  3. Add :inherited wherever a parent must continue affecting descendants, especially headers, confirmation, targets, and includes.
  4. Update event names and replace removed JavaScript helpers with browser APIs.
  5. Test 4xx and 5xx responses because they now swap by default.
  6. Test hx-delete, which no longer includes enclosing form data unless requested with hx-include.
  7. Test back/forward navigation, out-of-band ordering, timeouts, queues, and every installed extension.

There are temporary escape hatches. implicitInheritance can restore the old inheritance default, noSwap can restore the old error policy, and the htmx-2-compat extension restores several 2.x behaviors and event names. These are useful for staging, not for ending the migration. A compatibility layer that remains forever leaves the application with two mental models.

The release itself models similar caution. Htmx 4 is published as version 4.0.0, but the project is keeping the 2.x line under npm’s latest tag and 4.x under next until early 2027. That avoids silently upgrading sites that use an unversioned CDN URL. Production HTML should pin an exact version regardless.

A Go-native companion: ghtmx

Go teams interested in this server-driven model should also look at ghtmx, created by 0xgosu—the author of this blog. It is a compiled template engine, forked from templ, that treats htmx as a compile-checked language feature rather than a collection of unchecked strings.

Route-aware hx-* bindings resolve against actual Go handlers, so renaming a route can break the build at every affected template. Compile-time fragment declarations produce both inline and standalone render entry points, while declared events generate typed emitters for HX-Trigger. The runtime itself uses only the Go standard library.

The current library is pre-1.0, and htmx 4.0 support is being added now. Check the ghtmx repository and changelog before adopting it for an htmx 4 application, and pin the version while its language and generated-code surface are still evolving.

What htmx 4 is really optimizing

The obvious headline is Fetch. The deeper theme is explicitness.

  • Inheritance must say that it crosses a boundary.
  • Error bodies are visible representations unless configured otherwise.
  • Multi-target updates name their targets and swap rules.
  • History either fetches a fresh page or opts into a cache.
  • Extensions register directly and use a consistent lifecycle.
  • Browser APIs replace library wrappers where the platform is sufficient.

These choices reduce invisible behavior without requiring the application to move its state into a client framework. That is the balance htmx is trying to hold: capable interactions, server authority, and HTML that still explains what the page can do.

Htmx 4 will not make every interface simpler. A graphics editor, offline-first workspace, or deeply collaborative local model may need a richer client architecture. But many business applications are primarily navigation, forms, tables, validation, and server-owned workflows. For those systems, returning the finished representation can be simpler than synchronizing two state machines.

The most impressive part of htmx 4 is therefore what users do not have to relearn. The hypermedia control is still an element. The response is still HTML. The browser is still a browser. The new runtime simply makes that old architecture more at home on the current web platform.

Further reading

Top comments (0)