htmx 4.0 is under construction — migration guide

Using Extensions

Install, configure, and build htmx extensions.

htmx supports extensions to augment its core hypermedia infrastructure. The extension mechanism takes pressure off the core library to add new features, allowing it to focus on its main purpose of generalizing hypermedia controls.

Using Extensions

In htmx 4, extensions hook into standard events rather than callback extension points. They are lightweight with no performance penalty.

Extensions apply page-wide without requiring hx-ext on parent elements. They activate via custom attributes where needed.

Loading an Extension

Include the extension script after htmx:

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

Or with a bundler:

import 'htmx.org'; import 'htmx.org/dist/ext/hx-sse';

Restricting Extensions

To restrict which extensions can register, use an allow list:

<meta name="htmx-config" content='{"extensions": "my-ext,another-ext"}'>

When this config is set, only the listed extensions will be loaded. Without it, all registered extensions are active.

Core Extensions

htmx supports a set of core extensions, maintained by the htmx development team and shipped with htmx:

ExtensionDescription
sseServer-Sent Events streaming
wsWebSocket bidirectional communication
head-supportMerge <head> tag information (styles, scripts) in htmx responses
preloadPreload content on hover or other events
browser-indicatorShow the browser’s native loading indicator during requests
alpine-compatCompatibility with Alpine.js
htmx-2-compatCompatibility layer for htmx 2.x code
optimisticOptimistic UI updates
upsertUpdate-or-insert swap strategy for dynamic lists
downloadSave responses as file downloads with streaming progress
ptagPer-element polling tags to skip unchanged content
targetsSwap the same response into multiple elements

Building Extensions

htmx 4 introduces an extension system based on event hooks rather than the old callback-based API.

Defining an Extension

Extensions are defined using htmx.registerExtension():

htmx.registerExtension("my-ext", { init: (internalAPI) => { // Called once when extension is registered // Store internalAPI reference if needed }, htmx_before_request: (elt, detail) => { // Called before each request // Return false to cancel }, htmx_after_request: (elt, detail) => { // Called after each request }, });

Event Hooks

Extensions hook into htmx lifecycle events. Event names use underscores instead of colons:

Core Lifecycle Events

Hook NameTriggered EventParametersDescription
htmx_before_inithtmx:before:init(elt, detail)Before element initialization
htmx_after_inithtmx:after:init(elt, detail)After element initialization
htmx_before_processhtmx:before:process(elt, detail)Before processing element
htmx_after_processhtmx:after:process(elt, detail)After processing element
htmx_before_cleanuphtmx:before:cleanup(elt, detail)Before cleaning up element
htmx_after_cleanuphtmx:after:cleanup(elt, detail)After cleaning up element

Request Lifecycle Events

Hook NameTriggered EventParametersDescription
htmx_config_requesthtmx:config:request(elt, detail)Configure request before sending
htmx_before_requesthtmx:before:request(elt, detail)Before request is sent
htmx_before_responsehtmx:before:response(elt, detail)After fetch, before body consumed
htmx_after_requesthtmx:after:request(elt, detail)After request completes
htmx_finally_requesthtmx:finally:request(elt, detail)Always called after request
htmx_errorhtmx:error(elt, detail)On request error

Swap Events

Hook NameTriggered EventParametersDescription
htmx_before_swaphtmx:before:swap(elt, detail)Before content swap
htmx_after_swaphtmx:after:swap(elt, detail)After content swap
htmx_before_settlehtmx:before:settle(elt, detail)Before settle phase
htmx_after_settlehtmx:after:settle(elt, detail)After settle phase
handle_swap(direct call)(swapStyle, target, fragment, swapSpec)Custom swap handler

History Events

Hook NameTriggered EventParametersDescription
htmx_before_history_updatehtmx:before:history:update(elt, detail)Before updating history
htmx_after_history_updatehtmx:after:history:update(elt, detail)After updating history
htmx_after_history_pushhtmx:after:history:push(elt, detail)After pushing to history
htmx_after_history_replacehtmx:after:history:replace(elt, detail)After replacing history
htmx_before_history_restorehtmx:before:history:restore(elt, detail)Before restoring from history

Cancelling Events

Return false or set detail.cancelled = true to cancel an event:

htmx.registerExtension("validator", { htmx_before_request: (elt, detail) => { if (!isValid(detail.ctx)) { return false; // Cancel request } }, });

Internal API

The init hook receives an internal API object with helper methods:

let api; htmx.registerExtension("my-ext", { init: (internalAPI) => { api = internalAPI; }, htmx_after_init: (elt) => { let value = api.attributeValue(elt, "hx-my-attr"); let specs = api.parseTriggerSpecs("click, keyup delay:500ms"); let { method, action } = api.determineMethodAndAction(elt, evt); }, });

Available internal API methods:

  • attributeValue(elt, name, defaultVal, returnElt) - Get htmx attribute value with inheritance
  • parseTriggerSpecs(spec) - Parse trigger specification string
  • determineMethodAndAction(elt, evt) - Get HTTP method and URL
  • createRequestContext(elt, evt) - Create request context object
  • collectFormData(elt, form, submitter) - Collect form data
  • handleHxVals(elt, body) - Process hx-vals attribute

Request Context

The detail.ctx object contains request information:

{ sourceElement, // Element triggering request sourceEvent, // Event that triggered request status, // Request status target, // Target element for swap swap, // Swap strategy request: { action, // Request URL method, // HTTP method headers, // Request headers body, // Request body (FormData) validate, // Whether to validate abort, // Function to abort request signal // AbortSignal }, response: { // Available after request raw, // Raw Response object status, // HTTP status code headers // Response headers }, text, // Response text (after request) hx // HX-* response headers (parsed) }

Custom Swap Strategies

Extensions can implement custom swap strategies:

htmx.registerExtension("my-swap", { handle_swap: (swapStyle, target, fragment, swapSpec) => { if (swapStyle === "my-custom-swap") { target.appendChild(fragment); return true; // Handled } return false; // Not handled }, });

Migrating from htmx 2.x

The htmx 4 extension API is completely different from htmx 2.x:

htmx 2.xhtmx 4Migration Notes
init(api)init(api)Same name, store API reference for other hooks
getSelectors()htmx_after_initCheck api.attributeValue(elt, "attr") instead of returning selectors
onEvent(name, evt)Specific hooksReplace with htmx_before_request, htmx_after_swap, etc. (use underscores)
transformResponse(text, xhr, elt)htmx_after_requestModify detail.ctx.text directly
isInlineSwap(swapStyle)Not neededMove logic into handle_swap
handleSwap(style, target, fragment)handle_swapArgs are (swapStyle, target, fragment, swapSpec), return truthy if handled
encodeParameters(xhr, params, elt)htmx_config_requestModify detail.ctx.request.body (FormData) and headers directly

Old API (htmx 2.x):

htmx.defineExtension("old", { onEvent: function (name, evt) {}, transformResponse: function (text, xhr, elt) {}, handleSwap: function (swapStyle, target, fragment, settleInfo) {}, });

New API (htmx 4):

htmx.registerExtension("new", { htmx_before_request: (elt, detail) => {}, htmx_after_request: (elt, detail) => {}, handle_swap: (swapStyle, target, fragment, swapSpec) => {}, });

Key differences:

  • Event-based hooks instead of method callbacks
  • Underscores in hook names (not colons)
  • Extensions load by including the script (config whitelist is optional)
  • Access to full request context via detail.ctx
  • Internal API provided via init hook