</> 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 htmx_handle_swap. For OOB outer swaps, use detail.unstripped
handleSwap(style, target, fragment)htmx_handle_swapAccess via detail.swapSpec.style and detail.fragment, return false
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
    },
});

Extension Approval

Extensions can be approved via the extensions config option in a meta tag:

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

If this is set then only approved extensions will be loaded. This prevents unauthorized extensions from running. By default without this config set in a meta tag all extensions will be approved.

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_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_after_restorehtmx:after:restore(elt, detail)After restoring content
htmx_handle_swaphtmx:handle:swap(elt, detail)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

SSE Events

Hook NameTriggered EventParametersDescription
htmx_before_sse_reconnecthtmx:before:sse:reconnect(elt, detail)Before SSE reconnection
htmx_before_sse_streamhtmx:before:sse:stream(elt, detail)Before SSE stream starts
htmx_after_sse_streamhtmx:after:sse:stream(elt, detail)After SSE stream ends
htmx_before_sse_messagehtmx:before:sse:message(elt, detail)Before processing SSE message
htmx_after_sse_messagehtmx:after:sse:message(elt, detail)After processing SSE message

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", {
    htmx_handle_swap: (target, detail) => {
        let { swapSpec, fragment } = detail;
        if (swapSpec.style === "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) => {},
    htmx_handle_swap: (elt, detail) => {},
});

Key differences: