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 Name | Triggered Event | Parameters | Description |
|---|---|---|---|
htmx_before_init | htmx:before:init | (elt, detail) | Before element initialization |
htmx_after_init | htmx:after:init | (elt, detail) | After element initialization |
htmx_before_process | htmx:before:process | (elt, detail) | Before processing element |
htmx_after_process | htmx:after:process | (elt, detail) | After processing element |
htmx_before_cleanup | htmx:before:cleanup | (elt, detail) | Before cleaning up element |
htmx_after_cleanup | htmx:after:cleanup | (elt, detail) | After cleaning up element |
Request Lifecycle Events
| Hook Name | Triggered Event | Parameters | Description |
|---|---|---|---|
htmx_config_request | htmx:config:request | (elt, detail) | Configure request before sending |
htmx_before_request | htmx:before:request | (elt, detail) | Before request is sent |
htmx_before_response | htmx:before:response | (elt, detail) | After fetch, before body consumed |
htmx_after_request | htmx:after:request | (elt, detail) | After request completes |
htmx_finally_request | htmx:finally:request | (elt, detail) | When request completes, fails, or is cancelled |
htmx_error | htmx:error | (elt, detail) | On request error |
Swap Events
| Hook Name | Triggered Event | Parameters | Description |
|---|---|---|---|
htmx_before_swap | htmx:before:swap | (elt, detail) | Before content swap |
htmx_after_swap | htmx:after:swap | (elt, detail) | After content swap |
htmx_finally_swap | htmx:finally:swap | (elt, detail) | After swap (success or error) |
htmx_before_settle | htmx:before:settle | (elt, detail) | Before settle phase |
htmx_after_settle | htmx:after:settle | (elt, detail) | After settle phase |
handle_swap | (direct call) | (swapStyle, target, fragment, swapSpec) | Custom swap handler |
History Events
| Hook Name | Triggered Event | Parameters | Description |
|---|---|---|---|
htmx_before_history_update | htmx:before:history:update | (elt, detail) | Before updating history |
htmx_after_history_update | htmx:after:history:update | (elt, detail) | After updating history |
htmx_after_history_push | htmx:after:history:push | (elt, detail) | After pushing to history |
htmx_after_history_replace | htmx:after:history:replace | (elt, detail) | After replacing history |
htmx_before_history_restore | htmx: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:
HCON- Parse and merge HCON valuesattributeValue(elt, name, defaultVal, returnElt)- Get an attribute value with inheritance supportparseTriggerSpecs(spec)- Parse a trigger specificationdetermineMethodAndAction(elt, evt)- Get{method, action}for an elementcreateRequestContext(elt, evt)- Create a request contextcollectFormData(elt, form, submitter, validate, isGet)- Collect form datagetAttributeObject(elt, name, callback, scope)- Read an object-valued attributeinsertContent(task, cssTransition)- Insert a swap task’s contentmorph(oldNode, fragment, innerHTML)- Morph existing contentisSoftMatch(oldNode, newNode)- Test whether two nodes can be morphedinitSecurity(ttPolicy, syncFn, asyncFn)- Configure Trusted Types and script constructorsonTrigger(elt, spec, handler)- Attach a parsed trigger handlerhtmxProp(elt)- Get an element’s internal htmx statetriggerHtmxEvent(elt, name, detail, bubbles)- Dispatch an htmx eventexecuteJavaScript(thisArg, values, code, expression, isAsync)- Execute JavaScript through htmx security policy
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, // FormData during htmx_config_request; final BodyInit later 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: https://developer.mozilla.org/en-US/docs/Web/API/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 }, });
For migrating extensions written for htmx 2.x, see Migrating Extensions to htmx 4.