</> htmx
🚧 htmx 4.0 is under construction. Read changes →

Building htmx Extensions

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

Migrating from htmx 2.x

If you’re migrating an extension from htmx 2.x, here’s a quick reference:

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 (check if exists first as not set for SSE responses)
isInlineSwap(swapStyle)Not neededMove logic into handle_swap. For OOB outer swaps, use detail.unstripped
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

For detailed migration examples, see the Extension Migration Guide.

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
    },
});

Loading Extensions

Extensions are loaded by including the script file. They apply page-wide automatically.

To restrict which extensions can register, use the extensions config as a whitelist:

<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.

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_push_into_historyhtmx:after:push:into:history(elt, detail)After pushing to history
htmx_after_replace_into_historyhtmx:after:replace:into:history(elt, detail)After replacing history
htmx_before_restore_historyhtmx:before:restore:history(elt, detail)Before restoring from history

View Transition Events

Hook NameTriggered EventParametersDescription
htmx_before_viewTransitionhtmx:before:viewTransition(elt, detail)Before view transition
htmx_after_viewTransitionhtmx:after:viewTransition(elt, detail)After view transition

Other Events

Hook NameTriggered EventParametersDescription
htmx_after_implicitInheritancehtmx:after:implicitInheritance(elt, detail)After implicit attribute inheritance

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) => {
        // Use internal API
        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:

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") {
            // Implement custom swap logic
            target.appendChild(fragment);
            return true; // Handled
        }
        return false; // Not handled
    },
});

Complete Example

(() => {
    let api;

    htmx.registerExtension("preload", {
        init: (internalAPI) => {
            api = internalAPI;
        },

        htmx_after_init: (elt) => {
            let preloadSpec = api.attributeValue(elt, "hx-preload");
            if (!preloadSpec) return;

            let specs = api.parseTriggerSpecs(preloadSpec);
            let eventName = specs[0].name;

            elt.addEventListener(eventName, async (evt) => {
                let ctx = api.createRequestContext(elt, evt);
                // Prefetch logic here
            });
        },

        htmx_before_request: (elt, detail) => {
            // Use prefetched response if available
            if (elt._htmx?.preload) {
                detail.ctx.fetch = () => elt._htmx.preload;
                delete elt._htmx.preload;
            }
        },

        htmx_before_cleanup: (elt) => {
            // Clean up listeners
            if (elt._htmx?.preloadListener) {
                elt.removeEventListener(
                    elt._htmx.preloadEvent,
                    elt._htmx.preloadListener,
                );
            }
        },
    });
})();

Migration from htmx 2.x

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

Old API (htmx 2.x):

htmx.defineExtension("old", {  // Note: htmx 2.x used defineExtension with different format
    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: