Breaking Changes

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>, or into the [hx-history-elt] element if one is present, the same behavior as htmx 2.

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.

hx-trigger queue modifier removed

The queue modifier on hx-trigger (e.g. hx-trigger="click queue:all") no longer works. Request queuing is now controlled exclusively by hx-sync.

<!-- htmx 2 --> <div hx-trigger="click queue:all" hx-get="/test">...</div> <!-- htmx 4: use hx-sync instead --> <div hx-trigger="click" hx-get="/test" hx-sync="this:queue all">...</div>

hx-trigger from: and target: selector quoting

In htmx 2, selectors with spaces in from: used parentheses: from:(form input). In htmx 4, HCON parses these modifier values, so selectors containing spaces or commas must be wrapped in single quotes:

<!-- htmx 2 --> <input hx-trigger="keyup from:(closest form)"> <!-- htmx 4 --> <input hx-trigger="keyup from:'closest form'">

Comma-separated selectors also need quoting: target:'.a, .b'.

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 Extension System for details.

Renames and Removals

Attributes

Do the hx-disable rename before upgrading. In htmx 2, hx-disable meant “skip htmx processing on this element”. In htmx 4, that role is hx-ignore, and hx-disable now does what hx-disabled-elt used to do.

htmx 2.xhtmx 4.xTypeNotes
hx-disablehx-ignorerenamedRename this first to avoid conflicts.
hx-disabled-elthx-disablerenamedRename this after hx-disable becomes hx-ignore.
hx-varshx-valsremovedUse the js: prefix.
hx-paramshtmx:config:requestremovedConfigure request parameters in the event.
hx-prompthx-prompt extensionremovedYou can also use a one-liner with hx-on.
hx-ext—removedInclude extension scripts directly.
hx-disinherit—removedNot needed. Inheritance is explicit.
hx-inherit—removedNot needed. Inheritance is explicit.
hx-requesthx-configremoved—
hx-history—removedNo localStorage.

Events

All events follow a new pattern: htmx:phase:action[:sub-action]. Most error events are consolidated to htmx:error. HTTP error responses use htmx:response:error.

htmx 2.xhtmx 4.xTypeNotes
htmx:afterOnLoadhtmx:after:initrenamed—
htmx:afterProcessNodehtmx:after:initrenamed—
htmx:afterRequesthtmx:after:requestrenamed—
htmx:afterSettlehtmx:after:settlerenamed—
htmx:afterSwaphtmx:after:swaprenamed—
htmx:beforeCleanupElementhtmx:before:cleanuprenamed—
htmx:beforeHistorySavehtmx:before:history:updaterenamed—
htmx:beforeOnLoadhtmx:before:initrenamed—
htmx:beforeProcessNodehtmx:before:processrenamed—
htmx:beforeRequesthtmx:before:requestrenamed—
htmx:beforeSwaphtmx:before:swaprenamed—
htmx:configRequesthtmx:config:requestrenamed—
htmx:historyCacheMisshtmx:before:history:restorerenamed—
htmx:historyRestorehtmx:before:history:restorerenamed—
htmx:loadhtmx:after:initrenamed—
htmx:oobAfterSwaphtmx:after:swaprenamed—
htmx:oobBeforeSwaphtmx:before:swaprenamed—
htmx:pushedIntoHistoryhtmx:after:history:pushrenamed—
htmx:replacedInHistoryhtmx:after:history:replacerenamed—
htmx:responseErrorhtmx:response:errorrenamedHTTP error responses use this event.
htmx:sendErrorhtmx:errorrenamed—
htmx:swapErrorhtmx:errorrenamed—
htmx:targetErrorhtmx:errorrenamed—
htmx:timeouthtmx:errorrenamed—
htmx:validation:validate—removedUse native browser form validation.
htmx:validation:failed—removedUse native browser form validation.
htmx:validation:halted—removedUse native browser form validation.
htmx:xhr:loadstart—removedhtmx uses fetch() now.
htmx:xhr:loadendhtmx:finally:requestremovedhtmx uses fetch() now.
htmx:xhr:progress—removedhtmx uses fetch() now.
htmx:xhr:aborthtmx:errorremovedhtmx uses fetch() now.

Config

htmx 2.xhtmx 4.xTypeNotes
defaultSwapStyledefaultSwaprenamed—
globalViewTransitionstransitionsrenamed—
historyEnabledhistoryrenamed—
includeIndicatorStylesincludeIndicatorCSSrenamed—
timeoutdefaultTimeoutrenamed—
defaultTimeout60000default changedWas 0 (no timeout).
defaultSettleDelay1default changedWas 20.
addedClass—removedThe htmx-added CSS class is still applied during swaps.
allowEval—removed—
allowNestedOobSwaps—removed—
allowScriptTags—removed—
attributesToSettle—removed—
defaultSwapDelay—removed—
disableSelectorhx-ignoreremoved—
getCacheBusterParam—removed—
historyCacheSize—removed—
ignoreTitle—removedStill works per swap via hx-swap="... ignoreTitle:true".
inlineStyleNonce—removedIndicator CSS now uses Constructable Stylesheets, so no nonce is needed.
methodsThatUseUrlParams—removed—
refreshOnHistoryMiss—removed—
responseHandlinghx-statusremovedUse with noSwap.
scrollBehavior—removed—
scrollIntoViewOnBoost—removed—
selfRequestsOnlyhtmx.config.moderemoved—
settlingClass—removedThe htmx-settling CSS class is still applied during swaps.
swappingClass—removedThe htmx-swapping CSS class is still applied during swaps.
triggerSpecsCache—removed—
useTemplateFragments—removed—
withCredentialshx-configremoved—
wsBinaryType—removed—
wsReconnectDelay—removed—

Request headers

htmx 2.xhtmx 4.xTypeNotes
HX-TriggerHX-Sourcechanged formatFormat is tagName#id, e.g. button#submit.
HX-TargetHX-Targetchanged formatFormat is tagName#id.
HX-Trigger-NameHX-Sourceremoved—
HX-Prompthx-prompt extensionrestored by extLoad the extension.
—HX-Request-Typenew"full" or "partial".
—AcceptnewCore requests send text/html; hx-sse adds text/event-stream.

Response headers

htmx 2.xhtmx 4.xTypeNotes
HX-Trigger-After-SwapHX-TriggerremovedYou can also use JavaScript.
HX-Trigger-After-SettleHX-TriggerremovedYou can also use JavaScript.
HX-TriggerHX-Triggerunchanged—
HX-LocationHX-Locationunchanged—
HX-Push-UrlHX-Push-Urlunchanged—
HX-RedirectHX-Redirectunchanged—
HX-RefreshHX-Refreshunchanged—
HX-Replace-UrlHX-Replace-Urlunchanged—
HX-RetargetHX-Retargetunchanged—
HX-ReswapHX-Reswapunchanged—
HX-ReselectHX-Reselectunchanged—

JavaScript API

htmx 2.xhtmx 4.xTypeNotes
htmx.addClass()element.classList.add()removedUse native JavaScript.
htmx.removeClass()element.classList.remove()removedUse native JavaScript.
htmx.toggleClass()element.classList.toggle()removedUse native JavaScript.
htmx.closest()element.closest()removedUse native JavaScript.
htmx.remove()element.remove()removedUse native JavaScript.
htmx.off()removeEventListener()removedhtmx.on() returns the callback.
htmx.location()htmx.ajax()removed—
htmx.defineExtension()htmx.registerExtension()renamed—
htmx.logAll()htmx.config.logAll = trueremovedhtmx logs via console.*.
htmx.logNone()—removed—
htmx.loggerconsole.*removedObservability tools capture console.error, console.warn, and console.log.
htmx.onLoad()htmx.onLoad()changedNow listens on htmx:after:process, not htmx:after:init.

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

What’s New

Attributes

AttributePurpose
hx-actionSpecify URL, with optional hx-method. Supports progressive enhancement via native action/method fallback
hx-methodSpecify HTTP method (overrides native method and formmethod)
hx-queryIssue a QUERY request, which sends parameters in the body
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.timeout(time): returns a promise that resolves after a delay (number ms, or interval string '500ms'/'1s'/'5m')

htmx.takeClass is removed from core. Equivalent functionality is exposed by the hx-live extension on the htmx.live namespace:

htmx.live.take(target, className, source) // strip class from `source`, add to `target` htmx.live.forEvent(...args) // race events/timeouts htmx.live.nextFrame() // promise that resolves on next animation frame htmx.live.q(selector) // jQuery-like proxy rooted at documentElement htmx.live.debounce(ms[, fn]) // global debounce htmx.live.refresh() // recompute every live expression

Inside hx-live/hx-on expression scope these are available unprefixed (take, forEvent, nextFrame, q, debounce, toggle) with the current element used as the implicit context, see the hx-live extension docs.

Auto-logged events

Internally-dispatched events route to the console as follows:

  • If detail.error is set on the event, output goes to console.error (the Error instance is inlined first when applicable, so DevTools renders the stack). This covers request failures, hx-on handler exceptions, and other thrown paths. Apps that listen for htmx:error get the same data via the event.
  • If detail.warn is set, output goes to console.warn.
  • Otherwise, the event is logged at console.log (silent by default; set htmx.config.logAll = true to surface).

This restores the htmx 2.x convention: if you want an internal failure path to show up in the console, fire an event with detail.error (or detail.warn); no per-site console.error needed.

Request Context in Events

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
htmx:after:viewTransitionAfter a view transition completes
htmx:finally:requestWhen request completes, fails, or is cancelled

Config keys

ConfigDefaultPurpose
extensions''Comma-separated list of allowed extension names
mode'same-origin'Fetch mode (replaces selfRequestsOnly)
inlineScriptNonce''Nonce for inline scripts
metaCharacter':'Separator character in attribute/event names
morphIgnore["data-htmx-powered"]Attribute name prefixes to preserve during morph
morphScanLimitMax elements to scan during morph matching
morphSkip'[hx-morph-skip]'CSS selector for elements to skip during morph
morphSkipChildren'[hx-morph-skip-children]'CSS selector for elements whose children to skip during morph

Core extensions

htmx 4 ships with a bunch of “core” extensions now. The SSE and WebSocket extensions have been significantly rewritten, and hx-multipart, hx-live, hx-targets, hx-ptag, hx-csp, hx-download, hx-prompt, and hx-history-cache are new. See their upgrade guides for details.

ExtensionCategoryDescription
hx-multipartStreaming HTMLStream HTML with multipart/mixed
hx-sseStreaming HTMLStream HTML with text/event-stream (SSE) (upgrade guide)
hx-wsStreaming HTMLStream HTML and send data over WebSockets (upgrade guide)
hx-browser-indicatorUXShow tab’s spinner with hx-browser-indicator
hx-liveUXOur own DOM-based reactive scripting solution
hx-pendingUXShow custom content during requests
hx-promptUXPrompt before requests with hx-prompt='Reason?'
hx-preloadPerformancePreload on hover (upgrade guide)
hx-history-cachePerformanceRestore back/forward pages from sessionStorage
hx-ptagPerformanceSkip unchanged polls with HX-PTag: "v42"
hx-downloadSwapsDownload files with hx-swap='download'
hx-headSwapsMerge <head> tags with hx-head='merge'
hx-targetsSwapsTarget many elements with hx-targets='.selector'
hx-upsertSwapsUpdate or insert elements with hx-swap='upsert'
htmx-2-compatCompatibilityRestore htmx 2.x defaults and event names on htmx 4
hx-alpine-compatCompatibilityRun htmx alongside Alpine.js without conflicts
hx-cspSecurityMake htmx work under strict Content Security Policy