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>

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-extremovedInclude extension scripts directly.
hx-disinheritremovedNot needed. Inheritance is explicit.
hx-inheritremovedNot needed. Inheritance is explicit.
hx-requesthx-configremoved
hx-historyremovedNo 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:validateremovedUse native browser form validation.
htmx:validation:failedremovedUse native browser form validation.
htmx:validation:haltedremovedUse native browser form validation.
htmx:xhr:loadstartremovedhtmx uses fetch() now.
htmx:xhr:loadendhtmx:finally:requestremovedhtmx uses fetch() now.
htmx:xhr:progressremovedhtmx 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.
addedClassremovedThe htmx-added CSS class is still applied during swaps.
allowEvalremoved
allowNestedOobSwapsremoved
allowScriptTagsremoved
attributesToSettleremoved
defaultSwapDelayremoved
disableSelectorhx-ignoreremoved
getCacheBusterParamremoved
historyCacheSizeremoved
ignoreTitleremovedStill works per swap via hx-swap="... ignoreTitle:true".
inlineStyleNonceremovedIndicator CSS now uses Constructable Stylesheets, so no nonce is needed.
methodsThatUseUrlParamsremoved
refreshOnHistoryMissremoved
responseHandlinghx-statusremovedUse with noSwap.
scrollBehaviorremoved
scrollIntoViewOnBoostremoved
selfRequestsOnlyhtmx.config.moderemoved
settlingClassremovedThe htmx-settling CSS class is still applied during swaps.
swappingClassremovedThe htmx-swapping CSS class is still applied during swaps.
triggerSpecsCacheremoved
useTemplateFragmentsremoved
withCredentialshx-configremoved
wsBinaryTyperemoved
wsReconnectDelayremoved

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