#
Appendix B: htmx Extensions Reference
This appendix provides a complete reference for htmx extensions, including official extensions maintained by the htmx team and guidance on creating custom extensions. Use this as a reference when extending htmx functionality in your ASP.NET Core applications.
#
B.1 Introduction to Extensions
#
What Are htmx Extensions?
Extensions add functionality to htmx by hooking into its request/response lifecycle. They can modify requests, transform responses, add new attributes, and change swap behavior.
#
Loading Extensions
Extensions can be loaded from CDN or installed via npm:
<!-- From unpkg CDN -->
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/ext/json-enc.js"></script>
<!-- From cdnjs -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.12/htmx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.12/ext/json-enc.min.js"></script>
# npm installation
npm install htmx.org
#
Enabling Extensions
Use the hx-ext attribute to enable extensions:
<!-- Enable for entire body -->
<body hx-ext="json-enc">
...
</body>
<!-- Enable multiple extensions -->
<body hx-ext="json-enc, loading-states, response-targets">
...
</body>
<!-- Enable for specific section -->
<div hx-ext="preload">
<a href="/page1" preload>Page 1</a>
<a href="/page2" preload>Page 2</a>
</div>
<!-- Disable inherited extension -->
<div hx-ext="ignore:json-enc">
<!-- json-enc disabled here -->
</div>
#
Extension Inheritance
Extensions enabled on a parent element apply to all descendants:
<body hx-ext="loading-states">
<!-- All elements inherit loading-states -->
<div hx-ext="json-enc">
<!-- This div has both loading-states AND json-enc -->
<div hx-ext="ignore:loading-states">
<!-- This div has only json-enc -->
</div>
</div>
</body>
#
B.2 Official Extensions
#
B.2.1 json-enc
Encodes request bodies as JSON instead of form-urlencoded.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js"></script>
Activation:
<body hx-ext="json-enc">
Usage:
<form hx-post="/api/artists" hx-ext="json-enc">
<input type="text" name="name" value="New Artist" />
<input type="number" name="rating" value="5" />
<button type="submit">Create</button>
</form>
Request body becomes:
{"name": "New Artist", "rating": "5"}
ASP.NET Core Integration:
public class CreateArtistRequest
{
public string Name { get; set; } = string.Empty;
public int Rating { get; set; }
}
public IActionResult OnPost([FromBody] CreateArtistRequest request)
{
// request.Name and request.Rating are populated from JSON
var artist = _artistService.Create(request.Name, request.Rating);
return Partial("_ArtistRow", artist);
}
Configure JSON options in Program.cs:
builder.Services.AddRazorPages()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
Notes:
- Sets
Content-Type: application/jsonautomatically - Nested objects require proper naming:
address.city,address.zip - Arrays use bracket notation:
tags[0],tags[1]
#
B.2.2 client-side-templates
Renders JSON responses using client-side template engines (Mustache, Handlebars, Nunjucks).
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/client-side-templates.js"></script>
<!-- Plus your template engine -->
<script src="https://unpkg.com/mustache@4.2.0"></script>
Activation:
<body hx-ext="client-side-templates">
Supported Engines:
Usage with Mustache:
<div hx-get="/api/artists"
hx-trigger="load"
mustache-template="artist-template">
</div>
<template id="artist-template">
<ul>
{{#artists}}
<li>{{name}} - {{albumCount}} albums</li>
{{/artists}}
</ul>
</template>
ASP.NET Core Endpoint:
public IActionResult OnGetApi()
{
var artists = _artistService.GetAll()
.Select(a => new { name = a.Name, albumCount = a.Albums.Count });
return new JsonResult(new { artists });
}
Usage with Handlebars:
<div hx-get="/api/stats"
handlebars-template="stats-template">
</div>
<template id="stats-template">
<div class="stats">
<p>Total: {{total}}</p>
{{#if hasMore}}
<button hx-get="/api/more">Load More</button>
{{/if}}
</div>
</template>
Array Rendering:
<template id="list-template">
{{#each items}}
<div class="item">
<h3>{{this.title}}</h3>
<p>{{this.description}}</p>
</div>
{{/each}}
</template>
Notes:
- Templates must be in
<template>tags - Template ID is referenced without
# - Server must return JSON (set
Content-Type: application/json)
#
B.2.3 path-deps
Automatically refreshes elements when other requests modify their dependencies.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/path-deps.js"></script>
Activation:
<body hx-ext="path-deps">
Attributes:
Usage:
<!-- This element depends on /artists path -->
<div hx-get="/artists/list"
hx-trigger="load"
path-deps="/artists">
Artist list loads here
</div>
<!-- When this form posts, it triggers refresh of dependent elements -->
<form hx-post="/artists">
<input name="name" />
<button>Add Artist</button>
</form>
<!-- Multiple dependencies -->
<div hx-get="/dashboard/stats"
hx-trigger="load"
path-deps="/artists, /albums, /tracks">
Dashboard stats
</div>
How It Works:
- Element declares dependencies via
path-deps - When any htmx request modifies a dependency path (POST, PUT, PATCH, DELETE)
- All elements depending on that path automatically refresh
Server-Side Trigger:
You can also trigger refreshes via response header:
public IActionResult OnPostCreate(string name)
{
var artist = _artistService.Create(name);
// Trigger path-deps refresh
Response.Headers.Append("HX-Trigger", "path-deps");
return Partial("_ArtistRow", artist);
}
#
B.2.4 class-tools
Provides advanced class manipulation with timing support.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/class-tools.js"></script>
Activation:
<body hx-ext="class-tools">
Attributes:
classes Attribute Syntax:
classes="operation:className:timing, ..."
Operations:
add- Add classremove- Remove classtoggle- Toggle class
Timing:
100ms- Milliseconds1s- Seconds&- After previous completes
Examples:
<!-- Add class after 1 second -->
<div classes="add:highlight:1s">
Highlights after 1 second
</div>
<!-- Remove class after 500ms -->
<div class="visible" classes="remove:visible:500ms">
Fades out after 500ms
</div>
<!-- Sequential operations -->
<div classes="add:fade-in:0s, remove:fade-in:1s &, add:complete:0s &">
Sequential class changes
</div>
<!-- Toggle on interval -->
<div classes="toggle:blink:500ms">
Blinks every 500ms
</div>
Animation Pattern:
<style>
.fade-in { animation: fadeIn 0.5s ease-out; }
.fade-out { animation: fadeOut 0.5s ease-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
</style>
<div hx-get="/content"
hx-target="this"
hx-swap="innerHTML"
classes="add:fade-out:0s, remove:fade-out:500ms &, add:fade-in:0s &">
Content with fade transition
</div>
#
B.2.5 loading-states
Provides enhanced loading state management with fine-grained control.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/loading-states.js"></script>
Activation:
<body hx-ext="loading-states">
Attributes:
Basic Usage:
<form hx-post="/save">
<input name="data" />
<!-- Button shows loading state -->
<button type="submit">
<span data-loading-class="hidden">Save</span>
<span data-loading class="hidden">Saving...</span>
</button>
</form>
Scoped Loading States:
<div id="search-section">
<input hx-get="/search"
hx-target="#results"
hx-trigger="keyup changed delay:300ms" />
<!-- Only shows when search-section has active request -->
<div data-loading data-loading-target="#search-section" class="hidden">
Searching...
</div>
<div id="results"></div>
</div>
Delay to Prevent Flicker:
<!-- Only show spinner if request takes > 200ms -->
<div data-loading data-loading-delay="200ms" class="hidden">
<div class="spinner"></div>
</div>
Path-Specific Loading:
<!-- Only show for /api/heavy endpoint -->
<div data-loading data-loading-path="/api/heavy" class="hidden">
Processing heavy request...
</div>
Complete Form Example:
<form hx-post="/api/submit" hx-ext="loading-states">
<fieldset data-loading-disable>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" data-loading-class="opacity-50">
<span data-loading-class="hidden">Submit</span>
<span data-loading class="hidden">
<svg class="animate-spin h-4 w-4">...</svg>
Submitting...
</span>
</button>
</fieldset>
<div data-loading data-loading-delay="500ms" class="hidden">
This is taking longer than expected...
</div>
</form>
#
B.2.6 preload
Preloads content on mouseenter or focus for faster perceived performance.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/preload.js"></script>
Activation:
<body hx-ext="preload">
Attributes:
Basic Usage:
<!-- Preload on hover -->
<a href="/page" hx-get="/page" hx-target="#content" preload>
Go to Page
</a>
<!-- Preload on mousedown (faster click response) -->
<button hx-get="/data" hx-target="#output" preload="mousedown">
Load Data
</button>
Navigation Pattern:
<nav hx-ext="preload">
<a href="/dashboard" hx-get="/dashboard" hx-target="#main" hx-push-url="true" preload>
Dashboard
</a>
<a href="/artists" hx-get="/artists" hx-target="#main" hx-push-url="true" preload>
Artists
</a>
<a href="/albums" hx-get="/albums" hx-target="#main" hx-push-url="true" preload>
Albums
</a>
</nav>
Configuration via meta tag:
<meta name="htmx-config" content='{
"preload": {
"timeout": 500,
"images": true,
"ignoreClass": "no-preload"
}
}'>
Notes:
- Preloaded content is cached in memory
- Don't use for content that changes frequently
- Consider server load with many preload links
- Use
preload="mousedown"for actions that happen on click
#
B.2.7 remove-me
Automatically removes elements from the DOM after a delay.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/remove-me.js"></script>
Activation:
<body hx-ext="remove-me">
Attributes:
Usage:
<!-- Remove after 5 seconds -->
<div class="notification" remove-me="5s">
Your changes have been saved!
</div>
<!-- Remove after 2 seconds -->
<div class="toast" remove-me="2s">
Item deleted
</div>
Toast Notification Pattern:
Server response includes self-removing toast:
public IActionResult OnPostDelete(int id)
{
_service.Delete(id);
Response.Headers.Append("HX-Trigger", JsonSerializer.Serialize(new {
showToast = new { message = "Item deleted", type = "success" }
}));
return Content("");
}
Toast partial (_Toast.cshtml):
@model (string Message, string Type)
<div class="toast toast-@Model.Type"
remove-me="5s"
hx-ext="remove-me">
@Model.Message
</div>
With CSS Animation:
<style>
.toast {
animation: slideIn 0.3s ease-out;
}
.toast[removing] {
animation: slideOut 0.3s ease-out;
}
</style>
<!-- The extension adds [removing] attribute before removal -->
<div class="toast" remove-me="3s">
Message
</div>
#
B.2.8 response-targets
Specifies different target elements for different HTTP response codes.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/response-targets.js"></script>
Activation:
<body hx-ext="response-targets">
Attributes:
Usage:
<form hx-post="/api/save"
hx-target="#success-message"
hx-target-422="#validation-errors"
hx-target-500="#server-error"
hx-target-error="#generic-error">
<input name="email" type="email" required />
<button type="submit">Save</button>
</form>
<div id="success-message"></div>
<div id="validation-errors"></div>
<div id="server-error"></div>
<div id="generic-error"></div>
ASP.NET Core Integration:
public IActionResult OnPost(CreateModel model)
{
if (!ModelState.IsValid)
{
Response.StatusCode = 422; // Unprocessable Entity
return Partial("_ValidationErrors", ModelState);
}
try
{
var result = _service.Create(model);
return Partial("_SuccessMessage", result);
}
catch (Exception ex)
{
Response.StatusCode = 500;
return Partial("_ServerError", ex.Message);
}
}
Wildcard Patterns:
<div hx-get="/api/data"
hx-target="#content"
hx-target-4*="#client-error"
hx-target-5*="#server-error">
Load
</div>
Complete Error Handling Example:
<div id="form-container">
<form hx-post="/api/register"
hx-target="#form-container"
hx-target-400="#validation-errors"
hx-target-409="#conflict-error"
hx-target-5*="#server-error"
hx-swap="outerHTML">
<div id="validation-errors"></div>
<div id="conflict-error"></div>
<input name="username" required />
<input name="email" type="email" required />
<button type="submit">Register</button>
</form>
<div id="server-error" class="hidden"></div>
</div>
#
B.2.9 head-support
Merges <head> elements from htmx responses into the document head.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/head-support.js"></script>
Activation:
<body hx-ext="head-support">
Supported Elements:
<title>- Updates page title<meta>- Merges meta tags<link>- Adds stylesheets<style>- Adds inline styles<script>- Executes scripts (with care)
Usage:
Server response can include <head> elements:
<!-- Response from /artists page -->
<head>
<title>Artists - Chinook Dashboard</title>
<meta name="description" content="Manage your artists">
<link rel="stylesheet" href="/css/artists.css">
</head>
<div id="content">
<!-- Page content -->
</div>
ASP.NET Core Partial with Head:
// _ArtistsPage.cshtml
@{
ViewData["Title"] = "Artists";
}
<head>
<title>@ViewData["Title"] - Chinook Dashboard</title>
</head>
<div class="artists-page">
@* Page content *@
</div>
Configuration:
<meta name="htmx-config" content='{
"headSupport": {
"mergeMode": "append"
}
}'>
Notes:
- Duplicate prevention: Elements with same key attributes aren't duplicated
- Title always replaces
- Meta tags merge by name/property
- Scripts execute once (tracked by src)
#
B.2.10 multi-swap
Enables swapping multiple elements from a single response.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/multi-swap.js"></script>
Activation:
<body hx-ext="multi-swap">
Usage:
Use hx-swap="multi:#id1:swapStyle,#id2:swapStyle":
<button hx-get="/update-all"
hx-swap="multi:#header:innerHTML,#content:innerHTML,#footer:innerHTML">
Update All Sections
</button>
Server Response:
<div id="header">New header content</div>
<div id="content">New main content</div>
<div id="footer">New footer content</div>
Alternative: Using hx-swap-oob
For most cases, hx-swap-oob is simpler:
<!-- Primary target -->
<button hx-get="/data" hx-target="#main">Load</button>
<!-- Response includes OOB swaps -->
<div id="main">Main content</div>
<div id="sidebar" hx-swap-oob="true">Updated sidebar</div>
<div id="stats" hx-swap-oob="innerHTML">Updated stats</div>
#
B.2.11 morphdom-swap
Uses morphdom for intelligent DOM diffing during swaps.
Installation:
<script src="https://unpkg.com/morphdom@2.7.0/dist/morphdom-umd.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/morphdom-swap.js"></script>
Activation:
<body hx-ext="morphdom-swap">
Usage:
<div hx-get="/content" hx-swap="morph">
Content that will be morphed
</div>
<!-- Or with innerHTML -->
<div hx-get="/content" hx-swap="morph:innerHTML">
Content
</div>
<!-- Outer morph (replace element) -->
<div hx-get="/content" hx-swap="morph:outerHTML">
Content
</div>
Benefits:
- Preserves focus state in form inputs
- Preserves scroll position
- Smoother transitions for dynamic content
- Preserves video/audio playback state
When to Use:
- Real-time updating content
- Forms with many inputs
- Content with embedded media
- Chat interfaces
- Live data displays
Example - Live Data Table:
<table hx-get="/live-data"
hx-trigger="every 2s"
hx-swap="morph:innerHTML">
<tbody>
<!-- Rows update without losing state -->
</tbody>
</table>
#
B.2.12 alpine-morph
Uses Alpine.js morph for swaps, preserving Alpine component state.
Installation:
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/alpine-morph.js"></script>
Activation:
<body hx-ext="alpine-morph">
Usage:
<div x-data="{ open: false }"
hx-get="/content"
hx-swap="morph">
<button @click="open = !open">Toggle</button>
<div x-show="open">
<!-- This state preserved during htmx swap -->
</div>
</div>
Notes:
- Requires Alpine.js 3.x
- Alpine state (
x-data) is preserved during swaps - Useful when combining htmx with Alpine for client-side interactivity
#
B.2.13 ws (WebSocket)
Enables WebSocket connections for real-time bidirectional communication.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/ws.js"></script>
Activation:
<div hx-ext="ws">
Attributes:
Basic Usage:
<div hx-ext="ws" ws-connect="/ws/chat">
<div id="messages">
<!-- Messages appear here -->
</div>
<form ws-send>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
Server sends HTML that gets swapped:
<div id="messages" hx-swap-oob="beforeend">
<div class="message">Hello from server!</div>
</div>
ASP.NET Core WebSocket Endpoint:
app.UseWebSockets();
app.Map("/ws/chat", async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocket(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
});
async Task HandleWebSocket(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
// Parse form data and create response HTML
var responseHtml = $@"
<div id=""messages"" hx-swap-oob=""beforeend"">
<div class=""message"">{HttpUtility.HtmlEncode(message)}</div>
</div>";
var responseBytes = Encoding.UTF8.GetBytes(responseHtml);
await webSocket.SendAsync(responseBytes, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
SignalR Integration:
For more robust real-time features, use SignalR:
// Hub
public class ChatHub : Hub
{
public async Task SendMessage(string message)
{
var html = $@"<div id=""messages"" hx-swap-oob=""beforeend"">
<div class=""message"">{HttpUtility.HtmlEncode(message)}</div>
</div>";
await Clients.All.SendAsync("ReceiveMessage", html);
}
}
// Client
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();
connection.on("ReceiveMessage", function(html) {
htmx.swap("#messages", html, { swapStyle: "beforeend" });
});
connection.start();
#
B.2.14 sse (Server-Sent Events)
Enables Server-Sent Events for server-to-client push.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/sse.js"></script>
Activation:
<div hx-ext="sse">
Attributes:
Basic Usage:
<div hx-ext="sse" sse-connect="/events">
<!-- Swapped when 'notifications' event received -->
<div sse-swap="notifications">
Notifications appear here
</div>
<!-- Swapped when 'stats' event received -->
<div sse-swap="stats">
Stats appear here
</div>
</div>
ASP.NET Core SSE Endpoint:
app.MapGet("/events", async context =>
{
context.Response.Headers.Append("Content-Type", "text/event-stream");
context.Response.Headers.Append("Cache-Control", "no-cache");
context.Response.Headers.Append("Connection", "keep-alive");
while (!context.RequestAborted.IsCancellationRequested)
{
// Send notification event
await context.Response.WriteAsync($"event: notifications\n");
await context.Response.WriteAsync($"data: <div>New notification at {DateTime.Now}</div>\n\n");
await context.Response.Body.FlushAsync();
await Task.Delay(5000);
}
});
Multiple Event Types:
app.MapGet("/dashboard-events", async context =>
{
context.Response.Headers.Append("Content-Type", "text/event-stream");
while (!context.RequestAborted.IsCancellationRequested)
{
// Stats event
var stats = await GetCurrentStats();
await context.Response.WriteAsync($"event: stats\n");
await context.Response.WriteAsync($"data: <div class=\"stats\">{stats.Total} items</div>\n\n");
// Notifications event
var notifications = await GetNotifications();
if (notifications.Any())
{
await context.Response.WriteAsync($"event: notifications\n");
await context.Response.WriteAsync($"data: <ul>{string.Join("", notifications.Select(n => $"<li>{n}</li>"))}</ul>\n\n");
}
await context.Response.Body.FlushAsync();
await Task.Delay(3000);
}
});
Client HTML:
<div hx-ext="sse" sse-connect="/dashboard-events">
<div class="panel">
<h3>Live Stats</h3>
<div sse-swap="stats">Loading...</div>
</div>
<div class="panel">
<h3>Notifications</h3>
<div sse-swap="notifications">No notifications</div>
</div>
</div>
#
B.2.15 debug
Enables debug logging for htmx operations.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/debug.js"></script>
Activation:
<body hx-ext="debug">
Usage:
Once enabled, all htmx events are logged to the browser console:
htmx:configRequest {elt: button, ...}
htmx:beforeRequest {xhr: XMLHttpRequest, ...}
htmx:afterRequest {xhr: XMLHttpRequest, successful: true, ...}
htmx:beforeSwap {xhr: XMLHttpRequest, target: div#content, ...}
htmx:afterSwap {xhr: XMLHttpRequest, target: div#content, ...}
Scoped Debugging:
<!-- Only debug this section -->
<div hx-ext="debug">
<button hx-get="/data">This is logged</button>
</div>
<button hx-get="/other">This is not logged</button>
Custom Logger:
htmx.logger = function(elt, event, data) {
if (console) {
console.log(`[htmx] ${event}`, { element: elt, data: data });
}
};
#
B.2.16 event-header
Includes the triggering event in request headers.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/event-header.js"></script>
Activation:
<body hx-ext="event-header">
Headers Added:
Usage:
<button hx-get="/action" hx-ext="event-header">
Click Me
</button>
Server receives header:
Triggering-Event: {"type":"click","target":"button","x":123,"y":456}
ASP.NET Core:
public IActionResult OnGet()
{
var eventJson = Request.Headers["Triggering-Event"].FirstOrDefault();
if (!string.IsNullOrEmpty(eventJson))
{
var evt = JsonSerializer.Deserialize<TriggeringEvent>(eventJson);
// Use event information
}
return Partial("_Content");
}
#
B.2.17 restored
Adds the restored event and class when content is restored from history.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/restored.js"></script>
Activation:
<body hx-ext="restored">
Behavior:
- Adds
.restoredclass to restored elements - Fires
htmx:restoredevent
Usage:
/* Style restored content differently */
.restored {
animation: highlight 1s ease-out;
}
document.body.addEventListener('htmx:restored', function(event) {
// Re-initialize components after history restore
initializeComponents(event.detail.elt);
});
#
B.2.18 disable-element
Disables specified elements during htmx requests.
Installation:
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/disable-element.js"></script>
Activation:
<body hx-ext="disable-element">
Attributes:
Usage:
<form hx-post="/save" hx-disable-element="#submit-btn, #cancel-btn">
<input name="data" />
<button id="submit-btn" type="submit">Save</button>
<button id="cancel-btn" type="button">Cancel</button>
</form>
Self-Disable:
<button hx-post="/action" hx-disable-element="this">
Click Me
</button>
#
B.3 Creating Custom Extensions
#
Extension API
htmx.defineExtension('my-extension', {
// Called when extension is initialized
init: function(api) {
// api provides htmx internals
},
// Called on htmx events
onEvent: function(name, event) {
// name: event name (e.g., 'htmx:configRequest')
// event: the event object
// Return false to prevent default handling
},
// Transform response before processing
transformResponse: function(text, xhr, elt) {
// Return modified text
return text;
},
// Check if this is a special swap style
isInlineSwap: function(swapStyle) {
return swapStyle === 'my-swap';
},
// Handle custom swap
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// Perform swap
// Return true if handled
},
// Encode parameters
encodeParameters: function(xhr, parameters, elt) {
// Modify parameters or xhr
// Return null to use default encoding
}
});
#
Example: Request Timing Extension
htmx.defineExtension('request-timing', {
onEvent: function(name, event) {
if (name === 'htmx:beforeRequest') {
event.detail.elt.dataset.requestStart = Date.now();
}
if (name === 'htmx:afterRequest') {
const start = parseInt(event.detail.elt.dataset.requestStart);
const duration = Date.now() - start;
console.log(`Request took ${duration}ms`);
// Add timing to element
event.detail.elt.dataset.lastRequestTime = duration;
// Dispatch custom event
event.detail.elt.dispatchEvent(new CustomEvent('requestTimed', {
detail: { duration }
}));
}
}
});
#
Example: Request Retry Extension
htmx.defineExtension('auto-retry', {
init: function() {
this.retryCount = new Map();
this.maxRetries = 3;
this.retryDelay = 1000;
},
onEvent: function(name, event) {
if (name === 'htmx:responseError' || name === 'htmx:sendError') {
const elt = event.detail.elt;
const path = event.detail.requestConfig?.path;
const key = `${elt.id || 'anon'}-${path}`;
const count = (this.retryCount.get(key) || 0) + 1;
this.retryCount.set(key, count);
if (count <= this.maxRetries) {
console.log(`Retrying request (${count}/${this.maxRetries})...`);
setTimeout(() => {
htmx.trigger(elt, 'htmx:trigger');
}, this.retryDelay * count);
return false; // Prevent default error handling
} else {
this.retryCount.delete(key);
}
}
if (name === 'htmx:afterRequest' && event.detail.successful) {
// Clear retry count on success
const elt = event.detail.elt;
const path = event.detail.requestConfig?.path;
const key = `${elt.id || 'anon'}-${path}`;
this.retryCount.delete(key);
}
}
});
#
Example: Offline Support Extension
htmx.defineExtension('offline-support', {
init: function() {
this.queue = [];
window.addEventListener('online', () => {
this.processQueue();
});
},
onEvent: function(name, event) {
if (name === 'htmx:configRequest') {
if (!navigator.onLine) {
// Queue the request
this.queue.push({
method: event.detail.verb,
path: event.detail.path,
parameters: event.detail.parameters,
elt: event.detail.elt
});
// Show offline message
event.detail.elt.dispatchEvent(new CustomEvent('offlineQueued'));
return false; // Cancel the request
}
}
},
processQueue: function() {
while (this.queue.length > 0) {
const request = this.queue.shift();
htmx.ajax(request.method, request.path, {
values: request.parameters
});
}
}
});
#
B.4 Extension Compatibility
#
Browser Support
All official extensions support:
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
#
htmx Version Requirements
#
Known Conflicts
#
Quick Reference
#
Most-Used Extensions
#
Extension Loading Template
<!DOCTYPE html>
<html>
<head>
<title>htmx App</title>
</head>
<body hx-ext="json-enc, loading-states, response-targets">
<!-- Your content -->
<!-- Core htmx -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- Extensions -->
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/json-enc.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/loading-states.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.10/dist/ext/response-targets.js"></script>
</body>
</html>
#
Error Handling Pattern
<form hx-post="/api/save"
hx-ext="json-enc, response-targets, loading-states"
hx-target="#result"
hx-target-422="#validation-errors"
hx-target-5*="#server-error">
<div id="validation-errors"></div>
<input name="data" data-loading-disable />
<button type="submit">
<span data-loading-class="hidden">Save</span>
<span data-loading class="hidden">Saving...</span>
</button>
</form>
<div id="result"></div>
<div id="server-error"></div>
For detailed implementation examples, refer to the relevant chapters in the main text.