How to Use htmx Extensions: A Complete Guide to Customizing HTMX Behavior
Register extensions using htmx.defineExtension(name, ext) and activate them on elements with the hx-ext attribute.
htmx provides a modular extension system that lets you intercept requests, customize content swapping, and add new HTML attributes without modifying core library files. According to the bigskysoftware/htmx source code, the extension architecture centers around the defineExtension method located at approximately line 5004 of src/htmx.js, which maintains an internal registry mapping extension names to configuration objects.
Understanding the Extension Architecture
The htmx extension system comprises three core components working together during the request lifecycle.
Extension Registry. Located in src/htmx.js, the registry stores extension definitions added via htmx.defineExtension(name, extensionObject) and removed via htmx.removeExtension(name). This registry resolves extension names to their implementation objects when htmx processes elements.
Extension Loading. When htmx encounters an element with the hx-ext attribute, the internal withExtensions function (lines 5116-5142 in src/htmx.js) walks the comma-separated list of extension names, retrieves each from the registry, and associates it with that element's request lifecycle.
Hook Execution. Extensions implement optional hooks such as onEvent, isInlineSwap, and onSwap. During request processing—specifically within functions like handleSwap and triggerEvent—htmx invokes these hooks at precise points (before requests, during swaps, after responses) to allow extensions to modify or cancel behavior.
How to Register an Extension with defineExtension
Before using an extension, you must register it with the htmx runtime. The htmx.defineExtension method accepts two parameters: a string identifier and an object containing hook implementations.
// Register a simple logging extension
htmx.defineExtension("log-events", {
onEvent: function(name, evt) {
console.log(`[htmx:${name}]`, evt);
}
});
The extension object can implement any combination of these hooks:
onEvent(name, evt): Called for every htmx event (e.g.,htmx:beforeRequest,htmx:afterSwap), receiving the canonical event name and native Event objectisInlineSwap(swapStyle): Returns boolean indicating whether this extension handles the specified swap styleonSwap(swapStyle, target, fragment): Performs the actual DOM manipulation for custom swap stylesonBeforeRequest(config): Modifies request configuration before the AJAX callonAfterSwap(target): Executes cleanup or initialization after content insertion
Activating Extensions with hx-ext
Once registered, activate extensions by adding the hx-ext attribute to any HTML element. Multiple extensions can be comma-separated.
<!-- Single extension -->
<div hx-ext="log-events">
<button hx-get="/api/data">Load Data</button>
</div>
<!-- Multiple extensions -->
<form hx-ext="json-enc, loading-states" hx-post="/api/submit">
<input type="text" name="username">
<button type="submit">Submit</button>
</form>
Extensions inherit to child elements unless overridden. Use data-hx-ext as an alternative if you require valid HTML5 data attributes.
Complete Example: Custom Fade Swap Extension
Here is a practical implementation demonstrating custom swap handling and event interception:
// Define custom fade behavior
htmx.defineExtension("fade-swap", {
isInlineSwap: function(swapStyle) {
return swapStyle === "fade";
},
onSwap: function(swapStyle, target, fragment) {
if (swapStyle === "fade") {
target.style.opacity = "0";
target.innerHTML = "";
target.appendChild(fragment);
requestAnimationFrame(() => {
target.style.transition = "opacity 0.5s ease-in-out";
target.style.opacity = "1";
});
}
},
onEvent: function(name, evt) {
if (name === "htmx:beforeSwap") {
console.log("Starting fade transition...");
}
}
});
Usage in HTML:
<div hx-get="/panel" hx-swap="fade" hx-ext="fade-swap">
Content will fade in when updated
</div>
This extension registers the "fade" swap style (checked by isInlineSwap), implements the transition logic in onSwap, and logs lifecycle events via onEvent.
Built-in Extensions Available in htmx
The bigskysoftware/htmx repository ships with several production-ready extensions located in dist/ext/:
WebSocket Extension (ws). Enables real-time updates via WebSocket connections using the hx-ws attribute. Source: dist/ext/ws.js, registers with htmx.defineExtension("ws", ...).
Server-Sent Events (sse). Supports unidirectional server push through hx-sse attributes. Source: dist/ext/sse.js.
JSON Encoding (json-enc). Automatically serializes form submissions as JSON when using hx-encoding="json". Source: dist/ext/json-enc.js.
Method Override (method-override). Allows HTML forms to use HTTP verbs like PUT and DELETE via hx-override-method. Source: dist/ext/method-override.js.
Loading States (loading-states). Automatically applies CSS classes such as htmx-request to elements during active requests. Source: dist/ext/loading-states.js.
Debug (debug). Logs every htmx event to the browser console for development troubleshooting. Source: dist/ext/debug.js.
Include these extensions after the main htmx script:
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/json-enc.js"></script>
Implementation Checklist for htmx Extensions
Follow these steps to implement any htmx extension correctly:
- Load htmx core before defining extensions
- Define your extension object with the required hooks (
onEvent,onSwap, etc.) - Register the extension using
htmx.defineExtension('unique-name', extensionObject) - Attach
hx-ext="unique-name"to target elements in your HTML - Test the extension lifecycle by monitoring the specific hooks you implemented
Extensions defined later in the page lifecycle can still be applied to dynamically added content, as htmx processes hx-ext attributes during element initialization in the withExtensions pipeline.
Summary
- Use
htmx.defineExtension(name, config)(located at line ~5004 insrc/htmx.js) to register extension objects with the htmx runtime - Activate extensions via
hx-extattributes, supporting comma-separated lists for multiple extensions - Implement hooks such as
onEvent,isInlineSwap, andonSwapto intercept requests, define custom swap styles, and react to the htmx lifecycle - Reference built-in extensions in
dist/ext/for production patterns like WebSocket integration, JSON encoding, and loading state management - Process elements through
withExtensions(internal API, lines 5116-5142 insrc/htmx.js) which resolves extension names and attaches them to the request pipeline
Frequently Asked Questions
What is the difference between hx-ext and data-hx-ext?
Both attributes activate htmx extensions, but data-hx-ext conforms to HTML5 data attribute specifications while hx-ext uses htmx's custom attribute syntax. The functionality is identical; htmx processes both variants when resolving extensions through the internal attribute parsing in src/htmx.js.
Can I use multiple htmx extensions on the same element?
Yes. Separate extension names with commas in the hx-ext attribute (e.g., hx-ext="json-enc, loading-states"). The withExtensions function in src/htmx.js (lines 5116-5142) iterates through the list and registers each extension's hooks for that element's lifecycle.
When should I use isInlineSwap versus onSwap?
Use isInlineSwap(swapStyle) to declare that your extension handles a specific swap style (returning true for styles like "fade" or "morph"). Implement onSwap(swapStyle, target, fragment) to execute the actual DOM manipulation when that style is requested. The former acts as a capability check; the latter performs the work.
Where are the built-in htmx extensions located?
Built-in extensions reside in the dist/ext/ directory of the bigskysoftware/htmx repository. Each file (such as ws.js, sse.js, or json-enc.js) calls htmx.defineExtension() to self-register when loaded after the core htmx library.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →