<- blog

The htmx 4 Upgrade Reassigns UI State

Explicit inheritance, network history restores, error swaps, and morphing change who owns UI state. Migrate by provenance, not search-and-replace.

#web-platform#developer-tools#reliability#ux

htmx 4.0.0 was released on 28 August after an eight-month rewrite. From an application developer's viewpoint, much of the library still looks familiar: HTML attributes issue requests and returned HTML updates the page.

The important changes sit underneath that familiar shape. Attribute inheritance is explicit by default. History restoration re-fetches the server instead of reading page snapshots from localStorage. Error responses swap into the page unless configured otherwise. Requests use fetch() rather than XMLHttpRequest. Morphing can preserve live DOM nodes and user input while applying newer server HTML.

Those are not independent upgrade chores. Together they move responsibility for state among four places: declarative markup, the server response, the live browser DOM, and JavaScript event handlers.

The repeated angle to avoid

The ten most recent posts here covered Shopify reconciliation clocks, verified-bot policy joins, AI-catalog conformance, security-dashboard denominators, chat-adapter semantics, agent compute planes, security treatment states, soft-navigation measurement, authorization control loops, and credential containment. Older overlapping posts covered native form controls, pointer interaction contracts, and cache variants.

The weak version of this article would revive the old X needs Y formula: htmx upgrades need regression testing. It would also repeat the recent soft-navigation advice to test back, forward, and route transitions.

The sharper thesis is that htmx 4 changes which copy of UI state is authoritative at several boundaries. A parent attribute no longer silently supplies a child request. A back navigation can ask the server for today's representation instead of restoring yesterday's browser snapshot. A 422 response can become visible UI. A morph can retain what the person is typing even when the response contains another value. An old event listener can remain syntactically valid JavaScript while no longer supplying a header or recording an outcome.

The information surplus is the model joining those changes. The release notes list migration items one by one. The useful operator question is different: when two layers disagree, which value wins, and how will the application prove that was intentional?

Fresh evidence and background serve different jobs

The source map separates the release event from implementation mechanics and early boundary reports.

Source Freshness What it contributes
htmx 4.0.0 announcement 28 August 2026 Stable release, explicit inheritance, network-backed history, built-in morphing, fetch(), upgrade checker, support posture, and distribution decision
v4.0.0 GitHub release 28 August 2026 The shipped change set and same-release fixes across history, morphing, full-document responses, CSP, streams, forms, and events
Active-input morph fix Merged 28 August 2026 The explicit decision to protect a focused input from a stale server response while still morphing the surrounding form
HX-Request-Type cache separation Merged 23 August 2026 Full-versus-partial request classification, history-restore coverage, and the need to vary caches when CSP nonces differ
Hidden-document View Transition report and target:this report 29 August 2026 Fresh, open user reports showing why lifecycle and advanced-selector paths deserve tests; reports, not confirmed general release failures
What's New in htmx 4 Current primary documentation Exhaustive breaking changes, compatibility options, request/response behaviour, timeout, headers, events, and removals
Morphing guide and history-cache extension Current primary documentation Node identity, focused and unfocused input treatment, morph exclusions, optional sessionStorage, cache events, misses, and head restoration

The announcement is unusually conservative about adoption. htmx 2 will continue to be supported, and 4.0 is published under npm's next tag rather than latest until at least early 2027 so unversioned CDN users are not forced across a major boundary. That removes the worst migration deadline: there is no reason to trade production evidence for upgrade speed.

One interaction can contain four competing state owners

Consider a server-rendered edit form that refreshes validation or pricing while the user types.

markup policy
  parent supplies target, headers, confirmation, and request timing

server state
  response supplies canonical validation, totals, permissions, and HTML

live DOM state
  browser holds focus, text selection, unsent input, widget state, and scroll

event state
  JavaScript injects tokens, reroutes errors, initializes widgets, and records telemetry

In htmx 2, several defaults made those layers appear to cooperate automatically. Parent attributes inherited. Non-success responses generally did not swap. History often restored a serialized DOM. XHR-oriented listeners observed and modified request lifecycles.

htmx 4 makes more of those choices explicit. That is valuable because explicit policy is easier to inspect. It also means an upgrade can reveal dependencies that were never documented.

The migration task is not to select one universal owner. Different fields need different authority:

State Normal authority When another layer may temporarily win
CSRF or authorization requirement Current server and security policy Markup or a request event can transport the current token, but cannot weaken the server check
Request target and swap mode Explicit element or inherited markup policy A response status or pre-swap handler may route a known error to a bounded target
Product price, permission, inventory, booking availability Server at request time Live UI may display an optimistic or stale value only when visibly marked and later reconciled
Focused text being edited User's live DOM state Server validation may annotate it, but should not silently overwrite newer typing
Unfocused field after validation Declared form policy Server may normalize it if the application has defined when normalization is authoritative
Back-navigation representation Product decision Server re-fetch gives freshness; a browser cache gives continuity. Neither is automatically correct for every route
Analytics completion Verified request, swap, and business outcome events A click or request start can diagnose the path but cannot prove the business action completed

This table is the core migration asset. Search-and-replace can update names. It cannot decide these ownership rules.

Explicit inheritance turns DOM structure into visible request policy

The largest markup change is explicit attribute inheritance. In htmx 2, a parent could supply hx-target, hx-confirm, hx-include, hx-headers, hx-swap, or hx-boost to descendant actions. In htmx 4, an attribute that should flow down the DOM tree needs the :inherited modifier.

- <section hx-target="#result" hx-headers='{"X-CSRF-Token":"..."}'>
-   <button hx-post="/quote/recalculate">Recalculate</button>
- </section>
+ <section
+   hx-target:inherited="#result"
+   hx-headers:inherited='{"X-CSRF-Token":"..."}'
+ >
+   <button hx-post="/quote/recalculate">Recalculate</button>
+ </section>

The visible failure might be a response appearing in the wrong place. The more consequential failure is a child request losing security, form, confirmation, or concurrency context.

The shipped upgrade checker flags likely inheritance and renamed APIs:

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

Pinning the version makes the scan reproducible. The checker supports common HTML, JavaScript, TypeScript, PHP, Jinja, ERB, and Handlebars extensions, with extra extensions available through --ext.

Use its output as an inventory, not proof. Static analysis cannot reliably establish that:

  • a server-side partial composes with a parent template at runtime;
  • a CMS or component library emits an attribute in production;
  • one child is deliberately exempt from a shared confirmation;
  • a request header is also added by an event listener;
  • a target exists only after another swap;
  • a token source remains current after login, logout, or session rotation.

A useful review asks why each inherited attribute belongs at its current scope. If every destructive action under a container really shares one confirmation and token policy, make that propagation explicit. If only some actions do, move the attributes closer to those elements instead of restoring broad implicit inheritance globally.

htmx offers htmx.config.implicitInheritance = true as a compatibility bridge. That can reduce migration blast radius, but it should have an owner and removal condition. Otherwise the application runs htmx 4 while preserving the exact invisible dependency the new default was designed to expose.

Request semantics changed beyond fetch()

The internal move from XMLHttpRequest to fetch() is not just a transport implementation detail for applications that use hooks, extensions, uploads, progress events, timeouts, or custom headers.

The current docs identify several related changes:

  • XHR progress and abort event families are gone;
  • event names follow htmx:phase:action[:sub-action];
  • request and swap details now live under event.detail.ctx;
  • the default timeout is 60 seconds rather than unlimited;
  • custom extensions require the htmx 4 extension interface;
  • hx-delete, like hx-get, no longer includes the enclosing form automatically;
  • request source and target headers have changed format;
  • HX-Request-Type distinguishes full and partial requests.

A listener can therefore fail without a syntax error:

// Old listener: no longer supplies the header in htmx 4
document.addEventListener('htmx:configRequest', (event) => {
  event.detail.headers['X-CSRF-Token'] = currentToken();
});

// htmx 4
document.addEventListener('htmx:config:request', (event) => {
  event.detail.ctx.request.headers['X-CSRF-Token'] = currentToken();
});

Do not test this by checking only that a request left the browser. Assert the complete request contract at the server or in an end-to-end test:

method
+ final URL
+ request type
+ credentials and CSRF evidence
+ included form fields
+ content type
+ timeout or cancellation outcome
+ response status
+ selected target
+ resulting DOM

The hx-delete change deserves its own test where a delete action relied on hidden fields in an enclosing form. Add hx-include="closest form" only when sending those fields is intentional. Blindly restoring every surrounding field can widen the server input surface and make endpoint behaviour depend on unrelated form controls.

Error responses are now part of the rendering contract

htmx 4 swaps every HTTP response except 204 No Content and 304 Not Modified by default. In htmx 2, 4xx and 5xx responses did not normally swap.

This can be an improvement for form-heavy sites. A server can return a 422 with the invalid fields, accessible explanations, and preserved values, and the browser can render that response without pretending validation succeeded.

It can also expose accidental representations. A generic framework error page, authentication gateway response, proxy HTML, or full-document 500 was not necessarily designed to replace a quote form, cart summary, or booking panel.

Classify responses by both status and representation:

Response class Default migration decision Required test
2xx partial HTML Swap into the declared target Correct target, focus, live-region announcement, and event completion
204 intentional no-content result No swap Business action succeeded once; UI still exposes the resulting state
304 validator result No body swap Existing representation remains valid and request instrumentation closes cleanly
4xx field or business validation HTML Usually swap into a bounded form/error target Values preserved, errors associated with fields, no success analytics
401 or 403 session/permission response Explicit policy Login, reauthorization, or denial appears in the right surface without leaking private details
404 partial miss Explicit error target or no swap Surrounding page remains operable and canonical navigation still works
5xx application or proxy page Usually prevent arbitrary target replacement Safe message, correlation ID, retry policy, and server-side error evidence

The library provides hx-status and a noSwap configuration for status-specific treatment. The right answer is not necessarily to restore the htmx 2 default globally. It is to make intended error HTML a first-class response and reject representations that were never designed as fragments.

Test response content as well as status. A 200 login page returned after a session redirect can be more damaging to a partial UI than a well-formed 401. A 500 fragment designed for an inline error can be safer than a 200 full document returned by a misconfigured proxy.

The back button now asks the server a fresh question

By default, htmx 4 no longer saves history pages to localStorage. On back or forward navigation, it re-fetches the target and swaps the response into body or the configured history element. A full reload and disabled history are separate configuration options.

This changes what “back” means.

snapshot restore
  question: what DOM did this browser previously see?

network restore
  question: what representation does the server return for this URL now?

Both are legitimate. They disagree in important cases:

  • inventory, price, availability, or permissions changed;
  • the user logged out in another tab;
  • a draft had unsaved client state;
  • a multi-step form used a POST result that cannot be reconstructed from its URL;
  • the network is offline or slow;
  • a CDN serves a full page where htmx expects a partial response;
  • a response contains a fresh CSP nonce while an intermediary reuses the wrong variant;
  • analytics treats the restore as a new acquisition or page step.

The fresh HX-Request-Type change makes the cache boundary concrete. htmx classifies requests as full or partial after request context modifications, includes that classification on history restoration, and the CSP extension documentation calls for Vary: HX-Request-Type where cache separation is required. Without a correct variant key, a cache can return bytes generated for another rendering context.

This connects directly to the older cache-contract lesson without repeating it: in an htmx migration, the variant is not merely language or format. It can be document versus fragment, with different wrappers, scripts, head metadata, nonce treatment, and swap safety.

Write a route decision before choosing the history mode:

Route class Prefer network restore when Prefer optional history cache when
Public content or catalog Freshness and canonical server output matter more than exact prior DOM Offline continuity or instant return is a measured requirement
Search/filter results URL fully describes filters and server can reconstruct the view Client-only state is deliberate and safely serializable
Quote or booking step Availability and authorization must be rechecked A recoverable draft is expected and sensitive data is excluded or protected
Account/admin page Current permission must win Usually avoid storing sensitive page snapshots without a specific threat and retention review
Confirmation/result page Server can idempotently retrieve the completed result Cached receipt is non-sensitive and clearly marked as historical

If the product requires snapshot-like restoration, htmx 4 provides an optional history-cache extension. It stores up to ten pages in sessionStorage by default, exposes save/hit/miss/restore events, supports route exclusions, and can bypass stale hits. That is a deliberate client cache with explicit policy—not a reason to restore old behaviour everywhere.

Morphing makes disagreement visible inside the DOM

Built-in innerMorph and outerMorph swaps update an existing DOM tree towards the shape of server HTML instead of replacing the target wholesale. The algorithm uses matching IDs and descendant ID sets to retain node identity where possible.

That can preserve focus, text selection, scroll position, video playback, web-component state, and third-party widget state. It also creates a precise conflict: what happens when the server and live DOM both have a value?

The morphing guide and a fix merged immediately before the release document the policy:

  • the value of the currently focused input or textarea is preserved even if the response contains another value;
  • for unfocused inputs, the user's value is retained unless the returned value attribute changes;
  • stable IDs help the algorithm retain important elements;
  • hx-morph-skip and hx-morph-skip-children can exclude widgets or subtrees;
  • the sibling scan limit creates a performance-versus-matching trade-off for elements without IDs.

The focused-input rule protects against a classic stale-response race:

user types "Bris"
  -> validation request starts
  -> user continues to "Brisbane"
  -> response generated from "Bris" arrives
  -> surrounding form morphs
  -> focused field remains "Brisbane"

That is the correct default for active typing. It does not settle every field policy. A server may normalize a postcode, reject a coupon, recalculate quantity, or remove an option that became unavailable. Decide whether each response is advisory, normalizing, or authoritative.

Stable IDs also become operational identifiers, not decoration. Reusing one ID for two logical records can preserve the wrong node state. Changing an ID on every render defeats continuity. Leaving complex widgets entirely positional makes retention depend on sibling shape and scan limits.

For each morphed region, test:

  1. focused input changes after the request starts;
  2. unfocused dirty input versus a changed server value attribute;
  3. item insertion, deletion, and reordering with stable IDs;
  4. a third-party widget or custom element that keeps internal state;
  5. validation errors appearing without moving focus unexpectedly;
  6. server removal of an option the user currently selected;
  7. rapid overlapping requests returning out of order;
  8. browser back and forward after a morph;
  9. reduced-motion and hidden-document View Transition paths if transitions are enabled.

The open hidden-document issue from 29 August reports that a skipped View Transition can produce an unhandled promise rejection while the DOM update still completes. That is one report, not evidence that ordinary swaps fail. It is useful as a test design: switch tabs or hide the document while a delayed response is in flight, then assert DOM result, promise handling, telemetry, and user-visible state separately.

Observability must survive the event rename

Renaming an event listener is easy to treat as mechanical. It is dangerous when the listener carries business or security work.

htmx 4 exposes structured request, response, swap, element, history, and finalization events. htmx:response:error covers HTTP statuses of 400 or higher; htmx:error covers thrown request or swap exceptions; htmx:finally:request and htmx:finally:swap close lifecycles including failures.

Use those boundaries to distinguish:

intent observed
request configured
request sent
response received
response classified
swap planned
DOM updated
settle completed
business outcome verified

Do not record all eight as form_success. A request can succeed while a swap is cancelled. A swap can finish while the server rejected the business action with a rendered 422. A 204 can represent a successful mutation with no DOM bytes. A history restore can fetch a route without creating a new lead, order, or booking.

During migration, add temporary counters for old and new paths:

  • actions discovered by the upgrade checker;
  • requests by method, route class, and HX-Request-Type;
  • response statuses by intended swap policy;
  • request and swap finalizations without matching starts;
  • target-not-found and extension errors;
  • history network restores, cache hits, cache misses, and reloads;
  • morph conflicts where returned values differ from dirty inputs;
  • verified orders, leads, bookings, and account changes from their authoritative systems.

The objective is not permanent high-cardinality browser logging. It is a bounded migration window that proves the new event graph still joins to real outcomes.

A migration receipt for one route family

Migrate a coherent route family rather than flipping the whole site because one script tag worked locally.

htmx4_migration:
  route_family: quote_flow
  release: 4.0.0
  source_version: pinned_package_and_integrity
  old_behavior:
    implicit_inheritance: true
    history: local_snapshot
    error_swap: disabled_for_4xx_5xx
    transport: xhr
  intended_behavior:
    implicit_inheritance: false
    history: network_restore
    error_swap:
      422: form_target
      401: session_recovery_target
      5xx: safe_error_target
    transport: fetch
  state_authority:
    csrf: current_server_session
    quote_price: server_response
    focused_customer_input: live_dom
    availability: server_at_restore_time
    completion: crm_accepted_lead
  inherited_attributes_reviewed:
    - hx-target
    - hx-headers
    - hx-confirm
    - hx-sync
  cache_contract:
    vary:
      - HX-Request-Type
    partial_response_marker: expected_template_contract
    private_routes_publicly_cacheable: false
  morph_contract:
    stable_ids_reviewed: true
    third_party_widgets_excluded: true
    dirty_input_policy_tested: true
  compatibility_bridges:
    implicit_inheritance: temporary
    old_event_compatibility: temporary
    remove_after: all_route_tests_and_telemetry_pass
  evidence:
    upgrade_check: passed_with_reviewed_findings
    server_contract_tests: passed
    browser_matrix: passed
    back_forward_offline_tests: passed
    business_outcome_reconciliation: passed
  rollback:
    package_pin: previous_known_good
    markup_forward_compatible: confirmed

This receipt prevents a misleading result such as “the page looked right.” It records the intended authority, compatibility debt, cache variant, browser behaviour, and business proof.

Roll out by disagreement, not by page count

A small team can migrate safely without duplicating the entire application.

  1. Pin htmx 4.0.0 and run the official checker. Save the findings as a worklist; include generated templates and custom file extensions.
  2. Inventory server coupling. Search request-header reads, response-header writes, status-specific partials, full-versus-partial rendering, extensions, and old event names.
  3. Write the state-authority table. Cover security tokens, form data, server-calculated values, focused inputs, back-navigation state, and business outcomes.
  4. Select one route family. Prefer a representative but reversible surface before checkout, booking confirmation, account administration, or irreversible mutations.
  5. Make inheritance explicit. Scope shared attributes narrowly. Use compatibility mode only where it reduces a known migration risk.
  6. Classify every response status. Return fragment-safe HTML where swaps are intended and prevent arbitrary error documents replacing bounded UI.
  7. Choose history by route. Test network restoration, full reload, or the optional sessionStorage cache against freshness, privacy, offline, and reconstructability requirements.
  8. Define DOM identity. Give stable IDs to logical records and stateful controls; exclude components whose internal state the morph cannot safely reconcile.
  9. Test races and lifecycle edges. Delay responses, type after requests start, reorder records, switch tabs, expire sessions, go offline, and press back through a completed mutation.
  10. Reconcile to verified outcomes. Compare leads, orders, bookings, and account changes with the authoritative backend rather than request or pageview counts.
  11. Observe a normal business cycle. Include the site's real browser mix, traffic peaks, campaign paths, and slow dependencies.
  12. Remove bridges deliberately. Delete implicit inheritance and old-event compatibility only after evidence shows no route still depends on them.

The decision rule is practical: upgrade when the new ownership model removes ambiguity you can test, not because 4.0 exists. htmx 2 remains supported, and the project deliberately avoided making 4.0 npm's default. Waiting while you build the route and state inventory is a valid engineering choice.

Failure modes worth rehearsing

The CSRF header was inherited from farther away than anyone remembered

The UI still renders and GET requests pass, but one descendant mutation loses the parent hx-headers value. Test the server-observed header on every mutation class; do not treat a visible button click as proof.

A 422 improves one form and a 500 replaces another panel

The team welcomes error swapping for validation and forgets that generic server failures now swap too. Define treatment by status and representation, with a safe error target and correlation evidence.

Back navigation repeats an effect instead of retrieving a result

A URL cannot reconstruct a POST-derived state, or a restore path hits an endpoint with side effects. History retrieval must be safe and idempotent. Use result resources or a full reload where the URL does not describe a retrievable representation.

A CDN confuses the document and fragment variants

A history restore receives cached full-page HTML, or a normal navigation receives a fragment. Emit and honour the correct variant contract, including Vary: HX-Request-Type where applicable, and test through the real edge rather than only the origin.

Morphing preserves stale widget state under the wrong record

List items reorder without stable IDs and a stateful child is matched positionally. Give logical records stable IDs, test insertions and reordering, and skip morphing for components with incompatible internal ownership.

Server normalization overwrites a valid correction—or never applies

A response races with continued typing, or an unfocused value changes on the server. Write field-level authority rules and test focused, dirty, blurred, normalized, rejected, and out-of-order cases separately.

The old event listener disappears from the control path

Request injection, error routing, widget initialization, or analytics depended on a camelCase htmx 2 event. Update the event and context shape, then assert its effect. Merely finding the new listener in source is not runtime proof.

The compatibility bridge becomes permanent architecture

implicitInheritance and the htmx 2 compatibility extension make rollout easier, so the team never removes them. Record each bridge, route coverage, owner, and deletion gate in the migration receipt.

The practical conclusion

htmx 4 preserves the library's basic promise: the server can keep returning HTML while modest declarative attributes provide richer interactions. Its major changes do not turn that model into a client-heavy framework. They make several formerly implicit state decisions visible.

Markup must say which behaviour descends. The server becomes the default source for history restoration. Error HTML joins the normal rendering path. fetch() and a new event context change interception and observability. Morphing preserves selected pieces of live browser state instead of treating every response as total replacement.

That is why the upgrade is larger than syntax and smaller than a rewrite. Map who owns each state, make request propagation explicit, classify responses, separate document and fragment caches, define stable DOM identity, and test the moments when user input, server truth, browser history, and asynchronous responses disagree.

A migration that passes those disagreement tests has gained something more valuable than a new version number: an explainable contract for how server-rendered UI state moves through the browser.

Need technical help?

I'm a software engineer who builds web apps, APIs, and AI tooling. If you've got a project or a problem to talk through, book a free 30-minute call.

Book time with me ->