htmx 4.0 is under construction — migration guide

Migration

Migrate from htmx 2.x to htmx 4.x.

Quick Start

There are two major behavioral changes between htmx 2.x and 4.x:

  • In htmx 2.0 attribute inheritance is implicit by default while in 4.0 it is explicity by default
  • In htmx 2.0, 400 and 500 response codes are not swapped by default, whereas in htmx 4.0 these requests will be swapped

Add these two config lines to restore htmx 2.x behavior:

<script> htmx.config.implicitInheritance = true; htmx.config.noSwap = [204, 304, '4xx', '5xx']; </script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@next/dist/htmx.min.js"></script>

implicitInheritance restores htmx 2’s implicit attribute inheritance. noSwap prevents swapping error responses.

Or load the htmx-2-compat extension, which restores implicit inheritance, old event names, and previous error-swapping defaults:

<script src="/path/to/htmx.js"></script> <script src="/path/to/ext/htmx-2-compat.js"></script>

Most htmx 2 apps should work with either approach. Then migrate incrementally using this guide.

Upgrade Checker

htmx 4 ships with a command-line tool scans your templates and JS files for htmx 2 code that needs updating. It checks for removed attributes, old event names, inheritance patterns, extension changes, etc.

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

By default, the tool scans .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb, and .hbs files.

Output is file:line format, clickable in most editors. You can add additional file types with the --ext option.

The tool requires Python 3.

What Changed

fetch() replaces XMLHttpRequest

All requests use the native fetch() API. This cannot be reverted.

Explicit inheritance

Add :inherited to any attribute that should inherit down the DOM tree.

<!-- htmx 2: implicit inheritance --> <div hx-confirm="Are you sure?"> <button hx-delete="/item/1">Delete</button> </div> <!-- htmx 4: explicit inheritance --> <div hx-confirm:inherited="Are you sure?"> <button hx-delete="/item/1">Delete</button> </div>

Works on any attribute: hx-boost:inherited, hx-target:inherited, hx-confirm:inherited, etc.

Use :append to add to an inherited value instead of replacing it:

<div hx-include:inherited="#global-fields"> <!-- appends .extra to the inherited value --> <form hx-include:inherited:append=".extra">...</form> </div>

Revert: htmx.config.implicitInheritance = true

Error responses swap

htmx 4 swaps all HTTP responses. Only 204 and 304 do not swap.

htmx 2 did not swap 4xx and 5xx responses. In htmx 4, if your server returns HTML with a 422 or 500, that HTML gets swapped into the target. Design your error responses to work as swap content, or use hx-status to control per-code behavior.

Revert: htmx.config.noSwap = [204, 304, '4xx', '5xx']

hx-delete excludes form data

Like hx-get, hx-delete no longer includes the enclosing form’s inputs.

Fix: add hx-include="closest form" where needed.

No history cache

History no longer caches pages in localStorage. When navigating back, htmx re-fetches the page and swaps it into <body>.

Use htmx.config.history = "reload" for a full page reload instead. Use htmx.config.history = false to disable.

OOB swap order

In htmx 2, out-of-band (hx-swap-oob) elements swapped before the main content.

In htmx 4, the main content swaps first. OOB and <hx-partial> elements swap after (in document order).

This matters if an OOB swap creates or modifies DOM that the main swap depends on. If your app relies on that ordering, restructure so each swap is independent.

60-second timeout

htmx 2 had no timeout (0). htmx 4 sets defaultTimeout to 60000.

Revert: htmx.config.defaultTimeout = 0

Extension loading

Include extension scripts directly. No attribute needed:

<script src="/path/to/htmx.js"></script> <script src="/path/to/ext/sse.js"></script>

Restrict which extensions can load:

<meta name="htmx-config" content='{"extensions": "sse, ws"}'>

Extension authors use htmx.registerExtension(name, methodMap) to register.

See Extensions documentation for details.

Renames and Removals

Rename hx-disable

Do this before upgrading. The name hx-disable has been reassigned:

  • In htmx 2, hx-disable meant “skip htmx processing on this element”
  • In htmx 4, that role is hx-ignore
  • The name hx-disable now does what hx-disabled-elt used to do (disable form elements during requests)

Rename in this order to avoid conflicts:

  1. Rename hx-disable to hx-ignore
  2. Rename hx-disabled-elt to hx-disable

Removed attributes

RemovedUse instead
hx-varshx-vals with js: prefix
hx-paramshtmx:config:request event
hx-prompthx-confirm with js: prefix
hx-extInclude extension script directly
hx-disinheritNot needed (inheritance is explicit)
hx-inheritNot needed (inheritance is explicit)
hx-requesthx-config
hx-historyRemoved (no localStorage)
hx-history-eltRemoved

Renamed events

All events follow a new pattern: htmx:phase:action[:sub-action]

All error events are consolidated to htmx:error.

htmx 2.xhtmx 4.x
htmx:afterOnLoadhtmx:after:init
htmx:afterProcessNodehtmx:after:init
htmx:afterRequesthtmx:after:request
htmx:afterSettlehtmx:after:swap
htmx:afterSwaphtmx:after:swap
htmx:beforeCleanupElementhtmx:before:cleanup
htmx:beforeHistorySavehtmx:before:history:update
htmx:beforeOnLoadhtmx:before:init
htmx:beforeProcessNodehtmx:before:process
htmx:beforeRequesthtmx:before:request
htmx:beforeSwaphtmx:before:swap
htmx:configRequesthtmx:config:request
htmx:historyCacheMisshtmx:before:history:restore
htmx:historyRestorehtmx:before:history:restore
htmx:loadhtmx:after:init
htmx:oobAfterSwaphtmx:after:swap
htmx:oobBeforeSwaphtmx:before:swap
htmx:pushedIntoHistoryhtmx:after:history:push
htmx:replacedInHistoryhtmx:after:history:replace
htmx:responseErrorhtmx:error
htmx:sendErrorhtmx:error
htmx:swapErrorhtmx:error
htmx:targetErrorhtmx:error
htmx:timeouthtmx:error

Removed events

Validation events are removed. Use native browser form validation:

  • htmx:validation:validate
  • htmx:validation:failed
  • htmx:validation:halted

XHR events are removed (htmx uses fetch() now):

RemovedUse instead
htmx:xhr:loadstartNo replacement
htmx:xhr:loadendhtmx:finally:request
htmx:xhr:progressNo replacement
htmx:xhr:aborthtmx:error

Config changes

Renamed:

htmx 2.xhtmx 4.x
defaultSwapStyledefaultSwap
globalViewTransitionstransitions
historyEnabledhistory
includeIndicatorStylesincludeIndicatorCSS
timeoutdefaultTimeout

Changed defaults:

Confightmx 2htmx 4
defaultTimeout0 (no timeout)60000 (60 seconds)
defaultSettleDelay201

Removed:

addedClass, allowEval, allowNestedOobSwaps, allowScriptTags, attributesToSettle, defaultSwapDelay, disableSelector (use hx-ignore), getCacheBusterParam, historyCacheSize, ignoreTitle (still works per-swap via hx-swap="... ignoreTitle:true"), methodsThatUseUrlParams, refreshOnHistoryMiss, responseHandling (use hx-status and noSwap), scrollBehavior, scrollIntoViewOnBoost, selfRequestsOnly (use htmx.config.mode), settlingClass, swappingClass, triggerSpecsCache, useTemplateFragments, withCredentials (use hx-config), wsBinaryType, wsReconnectDelay

The htmx-swapping, htmx-settling, and htmx-added CSS classes are still applied during swaps. The config keys to customize their names have been removed.

Request headers

htmx 2.xhtmx 4.xNotes
HX-TriggerHX-SourceFormat changed to tagName#id (e.g. button#submit)
HX-TargetHX-TargetFormat changed to tagName#id
HX-Trigger-NameremovedUse HX-Source
HX-PromptremovedUse hx-confirm with js: prefix
(new)HX-Request-Type"full" or "partial"
(new)AcceptNow explicitly text/html

Response headers

Removed:

  • HX-Trigger-After-Swap
  • HX-Trigger-After-Settle

Use HX-Trigger or JavaScript instead.

Unchanged: HX-Trigger, HX-Location, HX-Push-Url, HX-Redirect, HX-Refresh, HX-Replace-Url, HX-Retarget, HX-Reswap, HX-Reselect.

JavaScript API changes

Removed methods. Use native JavaScript:

htmx 2.xUse instead
htmx.addClass()element.classList.add()
htmx.removeClass()element.classList.remove()
htmx.toggleClass()element.classList.toggle()
htmx.closest()element.closest()
htmx.remove()element.remove()
htmx.off()removeEventListener() (htmx.on() returns the callback)
htmx.location()htmx.ajax()
htmx.logAll()htmx.config.logAll = true
htmx.logNone()htmx.config.logAll = false

Renamed: htmx.defineExtension() is now htmx.registerExtension().

Still available: htmx.ajax(), htmx.config, htmx.find(), htmx.findAll(), htmx.on(), htmx.onLoad(), htmx.parseInterval(), htmx.process(), htmx.swap(), htmx.trigger().

Note: htmx.onLoad() now listens on htmx:after:process, not htmx:after:init.

What’s New

Attributes

AttributePurpose
hx-actionSpecify URL (use with hx-method)
hx-methodSpecify HTTP method
hx-configPer-element request config (JSON or key:value syntax)
hx-ignoreDisable htmx processing (was hx-disable)
hx-validateControl form validation behavior

hx-swap scroll modifiers

The show and scroll modifiers no longer support the combined selector:position syntax. Use separate keys instead:

<!-- htmx 2 (broken in 4) --> <div hx-swap="innerHTML show:#other:top"></div> <!-- htmx 4 --> <div hx-swap="innerHTML show:top showTarget:#other"></div> <div hx-swap="innerHTML scroll:bottom scrollTarget:#other"></div>

hx-swap styles

<div hx-get="/data" hx-swap="innerMorph">...</div> <div hx-get="/data" hx-swap="outerMorph">...</div> <div hx-get="/text" hx-swap="textContent">...</div> <div hx-get="/remove" hx-swap="delete">...</div>
  • innerMorph / outerMorph: morph swaps using the idiomorph algorithm. Better for preserving state in complex UIs.
  • textContent: set the target’s text content (no HTML parsing).
  • delete: remove the target element entirely.

New aliases for existing swap styles (both old and new names work):

NewEquivalent to
beforebeforebegin
afterafterend
prependafterbegin
appendbeforeend

Status code swaps

Set different swap behavior per HTTP status code:

<form hx-post="/save" hx-status:422="swap:innerHTML target:#errors select:#validation-errors" hx-status:5xx="swap:none push:false"> <!-- form fields --> </form>

Available config keys: swap:, target:, select:, push:, replace:, transition:.

Supports exact codes (404), single-digit wildcards (50x), and range wildcards (5xx). Evaluated in order of specificity.

<hx-partial>

Target multiple elements from one response. An alternative to hx-swap-oob for when you need explicit control over targeting and swap strategy:

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

Each <hx-partial> specifies its own hx-target and hx-swap strategy. See Multi-Target Updates for full documentation.

View transitions

View Transitions API support is available but disabled by default.

Enable: htmx.config.transitions = true

JSX compatibility

Frameworks that don’t support : in attribute names can use metaCharacter to replace it:

htmx.config.metaCharacter = "-"; // hx-ws-connect instead of hx-ws:connect // hx-confirm-inherited instead of hx-confirm:inherited

JavaScript methods

  • htmx.forEvent(eventName, timeout): returns a promise that resolves when an event fires
  • htmx.takeClass(element, className, container): removes class from siblings, adds to element
  • htmx.timeout(time): returns a promise that resolves after a delay

Request context

All events provide a consistent ctx object with request/response information.

Events

EventFires
htmx:after:cleanupAfter element cleanup
htmx:after:history:updateAfter history update
htmx:after:processAfter element processing
htmx:before:responseBefore response body is read (cancellable)
htmx:before:settleBefore settle phase
htmx:after:settleAfter settle phase
htmx:before:viewTransitionBefore a view transition starts (cancellable)
htmx:after:viewTransitionAfter a view transition completes
htmx:finally:requestAlways fires after a request (success or failure)

Config keys

ConfigDefaultPurpose
extensions''Comma-separated list of allowed extension names
mode'same-origin'Fetch mode (replaces selfRequestsOnly)
inlineScriptNonce''Nonce for inline scripts
inlineStyleNonce''Nonce for inline styles
metaCharacter':'Separator character in attribute/event names
morphIgnore''CSS selector for elements to ignore during morph
morphScanLimitMax elements to scan during morph matching
morphSkip''CSS selector for elements to skip during morph
morphSkipChildren''CSS selector for elements whose children to skip during morph

Core extensions

htmx 4 ships with 9 core extensions. The SSE and WebSocket extensions have been significantly rewritten. See their upgrade guides for details.

ExtensionDescription
alpine-compatAlpine.js compatibility: initializes Alpine on fragments before swap
browser-indicatorShows the browser’s native loading indicator during requests
head-supportMerges head tag information (styles, etc.) in htmx requests
htmx-2-compatRestores implicit inheritance, old event names, and previous error-swapping defaults
optimisticShows expected content from a template before the server responds
preloadTriggers requests early (on mouseover/mousedown) for near-instant page loads (upgrade guide)
sseServer-Sent Events streaming support (upgrade guide)
upsertUpdates existing elements by ID and inserts new ones, preserving unmatched elements
wsBi-directional WebSocket communication (upgrade guide)

Checklist

  1. Optionally, add config options or load htmx-2-compat for backward compatibility
  2. Run the upgrade checker to get a full list of issues
  3. Rename hx-disable to hx-ignore, then hx-disabled-elt to hx-disable
  4. Replace removed attributes with alternatives
  5. Find/replace event names in JavaScript and hx-on attributes
  6. Replace removed API methods with native JS
  7. Update extensions
  8. Rename changed config keys
  9. Test error handling (4xx/5xx now swap by default)
  10. Test attribute inheritance
  11. Test history navigation

Migration Notes

Individual documentation pages include migration notes where features changed.

Look for these:

Changes in htmx 4.0

Get Help