#
Appendix A: htmx Command Reference
This appendix provides a complete reference for htmx attributes, headers, events, and JavaScript API. Use it as a quick lookup while building htmx applications with ASP.NET Core.
#
A.1 Core Attributes
These attributes initiate HTTP requests when triggered.
#
hx-get
Issues an HTTP GET request to the specified URL.
<button hx-get="/Artists?handler=List"
hx-target="#artist-list">
Load Artists
</button>
// Razor Pages handler
public IActionResult OnGetList()
{
var artists = _artistService.GetAll();
return Partial("_ArtistList", artists);
}
#
hx-post
Issues an HTTP POST request. Form values are sent in the request body.
<form hx-post="/Artists?handler=Create"
hx-target="#artist-list"
hx-swap="afterbegin">
@Html.AntiForgeryToken()
<input type="text" name="name" />
<button type="submit">Create</button>
</form>
public IActionResult OnPostCreate(string name)
{
var artist = _artistService.Create(name);
return Partial("_ArtistRow", artist);
}
#
hx-put
Issues an HTTP PUT request for full resource updates.
<form hx-put="/Artists/5?handler=Update"
hx-target="#artist-row-5">
@Html.AntiForgeryToken()
<input type="text" name="name" value="@Model.Name" />
<button type="submit">Update</button>
</form>
#
hx-patch
Issues an HTTP PATCH request for partial resource updates.
<button hx-patch="/Artists/5?handler=ToggleActive"
hx-target="#artist-row-5">
Toggle Status
</button>
#
hx-delete
Issues an HTTP DELETE request.
<button hx-delete="/Artists/5"
hx-target="#artist-row-5"
hx-swap="delete"
hx-confirm="Delete this artist?">
Delete
</button>
public IActionResult OnDelete(int id)
{
_artistService.Delete(id);
return Content(""); // Empty response for delete swap
}
#
URL Patterns
#
A.2 Targeting and Swapping
#
hx-target
Specifies the element to update with the response. Accepts CSS selectors.
<!-- Target by ID -->
<button hx-get="/data" hx-target="#content">Load</button>
<!-- Target closest table row -->
<button hx-get="/edit" hx-target="closest tr">Edit</button>
<!-- Target the button itself -->
<button hx-get="/status" hx-target="this">Refresh</button>
#
hx-swap
Specifies how the response content is swapped into the target.
<!-- Replace content inside #list -->
<button hx-get="/items" hx-target="#list" hx-swap="innerHTML">
Load
</button>
<!-- Replace entire row -->
<button hx-get="/edit/5" hx-target="#row-5" hx-swap="outerHTML">
Edit
</button>
<!-- Append to list -->
<button hx-get="/more" hx-target="#list" hx-swap="beforeend">
Load More
</button>
<!-- Delete row after successful delete -->
<button hx-delete="/items/5" hx-target="closest tr" hx-swap="delete">
Delete
</button>
#
Swap Modifiers
Add modifiers to hx-swap to control timing and scrolling.
<!-- Swap with animation delay -->
<div hx-get="/content" hx-swap="innerHTML swap:300ms settle:100ms">
<!-- Scroll to top after swap -->
<div hx-get="/page/2" hx-swap="innerHTML scroll:top">
<!-- Show bottom of target -->
<div hx-get="/messages" hx-swap="beforeend show:bottom">
#
hx-swap-oob
Enables out-of-band swaps. The response can update multiple elements.
<!-- In response HTML -->
<div id="main-content">Main content here</div>
<div id="stats" hx-swap-oob="true">
Updated stats: 42 items
</div>
<div id="notification" hx-swap-oob="innerHTML">
Operation successful!
</div>
OOB swap strategies:
#
hx-select
Selects a portion of the response to swap.
<!-- Only swap the #results portion of the response -->
<button hx-get="/search"
hx-target="#output"
hx-select="#results">
Search
</button>
#
hx-select-oob
Selects elements from response for out-of-band swaps.
<button hx-get="/data"
hx-target="#main"
hx-select-oob="#sidebar,#footer">
Load
</button>
#
A.3 Triggering Requests
#
hx-trigger
Specifies the event that triggers the request.
Standard DOM Events:
<!-- Click (default for buttons) -->
<button hx-get="/data" hx-trigger="click">Click Me</button>
<!-- Change (default for inputs/selects) -->
<select hx-get="/filter" hx-trigger="change">...</select>
<!-- Submit (default for forms) -->
<form hx-post="/save" hx-trigger="submit">...</form>
<!-- Keyboard events -->
<input hx-get="/search" hx-trigger="keyup">
<!-- Mouse events -->
<div hx-get="/preview" hx-trigger="mouseenter">Hover me</div>
Special Triggers:
<!-- Load on page render -->
<div hx-get="/stats" hx-trigger="load">Loading...</div>
<!-- Infinite scroll -->
<div hx-get="/more" hx-trigger="revealed" hx-swap="afterend">
Loading more...
</div>
<!-- Poll every 30 seconds -->
<div hx-get="/notifications" hx-trigger="every 30s">
Checking...
</div>
<!-- Intersection with threshold -->
<div hx-get="/lazy" hx-trigger="intersect threshold:0.5">
Lazy content
</div>
#
Trigger Modifiers
<!-- Debounced search (wait 300ms after typing stops) -->
<input type="text"
hx-get="/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results">
<!-- Fire only once -->
<button hx-get="/init" hx-trigger="click once">Initialize</button>
<!-- Listen to events from document body -->
<div hx-get="/refresh" hx-trigger="refreshData from:body">
Data content
</div>
<!-- Queue only the last request -->
<input hx-get="/autocomplete"
hx-trigger="keyup delay:100ms queue:last">
Queue Strategies:
#
Multiple Triggers
Separate multiple triggers with commas:
<input hx-get="/validate"
hx-trigger="change, keyup delay:500ms changed">
#
hx-confirm
Shows a confirmation dialog before the request.
<button hx-delete="/items/5"
hx-confirm="Are you sure you want to delete this item?">
Delete
</button>
#
hx-prompt
Shows a prompt dialog and sends the input value.
<button hx-post="/rename"
hx-prompt="Enter new name:"
hx-vals='{"id": 5}'>
Rename
</button>
The prompt value is sent in the HX-Prompt header.
#
A.4 Request Modifiers
#
hx-include
Includes additional element values in the request.
<!-- Include search input value -->
<button hx-get="/search"
hx-include="[name='query']">
Search
</button>
<!-- Include all form inputs -->
<button hx-get="/filter"
hx-include="closest form">
Apply Filters
</button>
<!-- Include multiple elements -->
<button hx-get="/report"
hx-include="#startDate, #endDate, #category">
Generate
</button>
#
hx-params
Filters which parameters are submitted.
<!-- Send only specific parameters -->
<form hx-post="/save" hx-params="name, email">
<input name="name" />
<input name="email" />
<input name="internal_id" /> <!-- Not sent -->
</form>
<!-- Exclude parameters -->
<form hx-post="/save" hx-params="not csrf_token">
...
</form>
<!-- Send no parameters -->
<button hx-post="/ping" hx-params="none">Ping</button>
#
hx-vals
Adds additional values to the request as JSON.
<!-- Static values -->
<button hx-post="/action"
hx-vals='{"type": "archive", "notify": true}'>
Archive
</button>
<!-- Dynamic values with JavaScript -->
<button hx-post="/save"
hx-vals='js:{timestamp: Date.now()}'>
Save
</button>
#
hx-headers
Adds custom HTTP headers to the request.
<button hx-get="/api/data"
hx-headers='{"X-Custom-Header": "value"}'>
Load
</button>
For anti-forgery tokens in ASP.NET Core:
// In site.js - add to all requests
document.body.addEventListener('htmx:configRequest', function(event) {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
if (token) {
event.detail.headers['RequestVerificationToken'] = token;
}
});
#
hx-sync
Synchronizes requests to prevent race conditions.
<!-- Only allow one search at a time -->
<input hx-get="/search"
hx-sync="closest form:abort">
<!-- Queue form submissions -->
<form hx-post="/save" hx-sync="this:queue">
...
</form>
<!-- Drop duplicate clicks -->
<button hx-post="/action" hx-sync="this:drop">
Submit
</button>
#
hx-encoding
Sets the encoding type for the request.
<!-- For file uploads -->
<form hx-post="/upload" hx-encoding="multipart/form-data">
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
#
A.5 History and Navigation
#
hx-push-url
Pushes a URL to the browser history.
<!-- Push the request URL -->
<a hx-get="/page/2" hx-push-url="true">Page 2</a>
<!-- Push a custom URL -->
<button hx-get="/search?q=test" hx-push-url="/search/test">
Search
</button>
<!-- Disable URL push -->
<a hx-get="/modal" hx-push-url="false">Open Modal</a>
#
hx-replace-url
Replaces the current URL in history (no new history entry).
<button hx-get="/tab/details" hx-replace-url="true">
Details Tab
</button>
#
hx-history
Controls whether the element's innerHTML is saved to history cache.
<!-- Don't save this element to history -->
<div hx-history="false">
Sensitive content not cached
</div>
#
hx-history-elt
Specifies which element's content to snapshot for history.
<body hx-history-elt>
<!-- Only body content is cached, not full page -->
</body>
#
A.6 Indicators and Feedback
#
hx-indicator
Specifies an element to show during the request.
<button hx-get="/data" hx-indicator="#spinner">
Load Data
</button>
<span id="spinner" class="htmx-indicator">Loading...</span>
/* htmx automatically adds this, but you can customize */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
Multiple indicators:
<button hx-get="/data" hx-indicator="#spinner, #overlay">
Load
</button>
#
hx-disabled-elt
Disables elements during the request.
<!-- Disable the button itself -->
<button hx-post="/save" hx-disabled-elt="this">
Save
</button>
<!-- Disable multiple elements -->
<form hx-post="/submit" hx-disabled-elt="find input, find button">
<input name="data" />
<button type="submit">Submit</button>
</form>
<!-- Disable closest form -->
<button hx-post="/save" hx-disabled-elt="closest form">
Save
</button>
#
CSS Classes Applied by htmx
/* Show loading state */
.htmx-request {
opacity: 0.5;
pointer-events: none;
}
/* Fade in new content */
.htmx-added {
animation: fadeIn 0.3s ease-out;
}
/* Indicator styling */
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-flex;
}
#
A.7 Inheritance and Scope
#
hx-boost
Converts standard anchors and forms to use AJAX.
<!-- Boost all links and forms in this container -->
<div hx-boost="true">
<a href="/page/2">Page 2</a> <!-- Now uses AJAX -->
<form action="/search" method="get">...</form> <!-- Now uses AJAX -->
</div>
<!-- Disable boost for specific elements -->
<div hx-boost="true">
<a href="/page/2">AJAX Link</a>
<a href="/download.pdf" hx-boost="false">Normal Link</a>
</div>
#
hx-inherit
Controls which attributes are inherited from ancestors.
<!-- Only inherit hx-target -->
<div hx-target="#output" hx-swap="innerHTML">
<button hx-get="/a" hx-inherit="hx-target">A</button>
<button hx-get="/b" hx-inherit="hx-target">B</button>
</div>
#
hx-disinherit
Prevents attribute inheritance.
<div hx-target="#main" hx-confirm="Are you sure?">
<!-- This button won't inherit hx-confirm -->
<button hx-get="/safe" hx-disinherit="hx-confirm">
Safe Action
</button>
<!-- This button inherits everything -->
<button hx-delete="/dangerous">
Dangerous Action
</button>
</div>
<!-- Disinherit all attributes -->
<button hx-get="/standalone" hx-disinherit="*">
Standalone
</button>
#
hx-preserve
Preserves an element across swaps (useful for video, audio, iframes).
<div id="content">
<video id="player" hx-preserve="true" src="video.mp4"></video>
<div id="info">Info that gets swapped</div>
</div>
#
A.8 Extensions
#
hx-ext
Enables htmx extensions for an element and its descendants.
<!-- Enable single extension -->
<body hx-ext="json-enc">
...
</body>
<!-- Enable multiple extensions -->
<body hx-ext="json-enc, loading-states">
...
</body>
<!-- Disable inherited extension -->
<div hx-ext="ignore:json-enc">
...
</div>
#
Built-in Extensions
#
hx-ws (WebSocket)
Establishes a WebSocket connection.
<div hx-ws="connect:/chat">
<div id="messages"></div>
<form hx-ws="send">
<input name="message" />
<button>Send</button>
</form>
</div>
#
hx-sse (Server-Sent Events)
Connects to a Server-Sent Events endpoint.
<div hx-sse="connect:/events">
<div hx-sse="swap:message">
<!-- Updated when 'message' event received -->
</div>
<div hx-sse="swap:notification">
<!-- Updated when 'notification' event received -->
</div>
</div>
#
A.9 Request Headers
Headers automatically sent by htmx with each request:
#
Detecting htmx Requests in ASP.NET Core
// In a Razor Page or Controller
public IActionResult OnGet()
{
if (Request.Headers.ContainsKey("HX-Request"))
{
// htmx request - return partial
return Partial("_Content", model);
}
// Regular request - return full page
return Page();
}
// Extension method for cleaner code
public static class HttpRequestExtensions
{
public static bool IsHtmx(this HttpRequest request)
{
return request.Headers.ContainsKey("HX-Request");
}
public static string? HxTarget(this HttpRequest request)
{
return request.Headers["HX-Target"].FirstOrDefault();
}
public static string? HxTrigger(this HttpRequest request)
{
return request.Headers["HX-Trigger"].FirstOrDefault();
}
}
// Usage
if (Request.IsHtmx())
{
var target = Request.HxTarget();
}
#
A.10 Response Headers
Headers the server can send to control htmx behavior:
#
HX-Location
Triggers client-side redirect without full page reload.
// Simple redirect
Response.Headers.Append("HX-Location", "/new-page");
// With options
Response.Headers.Append("HX-Location",
JsonSerializer.Serialize(new {
path = "/new-page",
target = "#content",
swap = "innerHTML"
}));
#
HX-Push-Url
Pushes URL to browser history.
Response.Headers.Append("HX-Push-Url", "/artists/5");
// Prevent URL push
Response.Headers.Append("HX-Push-Url", "false");
#
HX-Replace-Url
Replaces current URL in history.
Response.Headers.Append("HX-Replace-Url", "/current-state");
#
HX-Redirect
Triggers full page redirect.
Response.Headers.Append("HX-Redirect", "/login");
#
HX-Refresh
Triggers full page refresh.
Response.Headers.Append("HX-Refresh", "true");
#
HX-Retarget
Changes the target element.
// Retarget to error container on validation failure
if (!ModelState.IsValid)
{
Response.Headers.Append("HX-Retarget", "#error-container");
Response.Headers.Append("HX-Reswap", "innerHTML");
return Partial("_ValidationErrors", ModelState);
}
#
HX-Reswap
Changes the swap method.
Response.Headers.Append("HX-Reswap", "outerHTML");
#
HX-Reselect
Changes which part of response to select.
Response.Headers.Append("HX-Reselect", "#main-content");
#
HX-Trigger
Triggers client-side events after response is received.
// Simple event
Response.Headers.Append("HX-Trigger", "showMessage");
// Event with data
Response.Headers.Append("HX-Trigger",
JsonSerializer.Serialize(new {
showToast = new { message = "Saved!", type = "success" }
}));
// Multiple events
Response.Headers.Append("HX-Trigger",
JsonSerializer.Serialize(new {
showToast = new { message = "Created!" },
refreshList = true,
closeModal = true
}));
#
HX-Trigger-After-Settle
Triggers events after the settle phase.
Response.Headers.Append("HX-Trigger-After-Settle", "initComponents");
#
HX-Trigger-After-Swap
Triggers events after the swap phase.
Response.Headers.Append("HX-Trigger-After-Swap", "highlightNew");
#
Response Header Helper Class
public static class HtmxResponseExtensions
{
public static void HxTrigger(this HttpResponse response, string eventName)
{
response.Headers.Append("HX-Trigger", eventName);
}
public static void HxTrigger(this HttpResponse response, object events)
{
response.Headers.Append("HX-Trigger", JsonSerializer.Serialize(events));
}
public static void HxRedirect(this HttpResponse response, string url)
{
response.Headers.Append("HX-Redirect", url);
}
public static void HxRefresh(this HttpResponse response)
{
response.Headers.Append("HX-Refresh", "true");
}
public static void HxRetarget(this HttpResponse response, string selector)
{
response.Headers.Append("HX-Retarget", selector);
}
public static void HxReswap(this HttpResponse response, string swapStyle)
{
response.Headers.Append("HX-Reswap", swapStyle);
}
public static void HxPushUrl(this HttpResponse response, string url)
{
response.Headers.Append("HX-Push-Url", url);
}
public static void HxReplaceUrl(this HttpResponse response, string url)
{
response.Headers.Append("HX-Replace-Url", url);
}
}
// Usage
Response.HxTrigger(new {
showToast = new { message = "Success!", type = "success" },
closeModal = true
});
#
A.11 Events
#
Request Lifecycle Events
#
Swap and Settle Events
#
Other Events
#
Event Handling Examples
// Add anti-forgery token to all requests
document.body.addEventListener('htmx:configRequest', function(event) {
const token = document.querySelector('input[name="__RequestVerificationToken"]')?.value;
if (token) {
event.detail.headers['RequestVerificationToken'] = token;
}
});
// Handle custom events from HX-Trigger header
document.body.addEventListener('showToast', function(event) {
const { message, type } = event.detail;
showToast(message, type);
});
document.body.addEventListener('closeModal', function() {
document.querySelector('.modal')?.remove();
});
// Cancel request conditionally
document.body.addEventListener('htmx:confirm', function(event) {
if (!confirm(event.detail.question)) {
event.preventDefault();
}
});
// Modify swap behavior
document.body.addEventListener('htmx:beforeSwap', function(event) {
// Handle 404 errors
if (event.detail.xhr.status === 404) {
event.detail.shouldSwap = true;
event.detail.target = document.getElementById('error-container');
}
});
// Handle errors globally
document.body.addEventListener('htmx:responseError', function(event) {
console.error('Request failed:', event.detail.xhr.status);
showToast('An error occurred', 'error');
});
document.body.addEventListener('htmx:sendError', function(event) {
showToast('Network error. Please check your connection.', 'error');
});
// Initialize components after swap
document.body.addEventListener('htmx:afterSettle', function(event) {
// Re-initialize any JavaScript components in swapped content
initializeComponents(event.detail.target);
});
// Log all htmx activity (debugging)
document.body.addEventListener('htmx:afterRequest', function(event) {
console.log('Request completed:', {
url: event.detail.requestConfig.path,
successful: event.detail.successful,
status: event.detail.xhr.status
});
});
#
A.12 JavaScript API
#
htmx.ajax()
Programmatically issue an htmx request.
// Simple GET
htmx.ajax('GET', '/api/data', '#target');
// With options
htmx.ajax('POST', '/api/save', {
target: '#result',
swap: 'innerHTML',
values: { name: 'John', age: 30 },
headers: { 'X-Custom': 'value' }
});
// Returns a Promise
htmx.ajax('GET', '/data', '#output')
.then(() => console.log('Request complete'));
#
DOM Manipulation
// Find elements
const el = htmx.find('#myId');
const els = htmx.findAll('.myClass');
const parent = htmx.closest(el, '.container');
// Class manipulation with transitions
htmx.addClass(el, 'active'); // Add class
htmx.removeClass(el, 'active'); // Remove class
htmx.toggleClass(el, 'active'); // Toggle class
htmx.takeClass(el, 'active'); // Take from siblings
// Remove element with transition
htmx.remove(el);
// Swap content manually
htmx.swap('#target', '<p>New content</p>', { swapStyle: 'innerHTML' });
#
Events
// Add event listener
htmx.on('htmx:afterSwap', function(event) {
console.log('Swapped:', event.detail.target);
});
// Remove event listener
htmx.off('htmx:afterSwap', handler);
// Trigger event
htmx.trigger(el, 'customEvent', { data: 'value' });
// Trigger htmx request on element
htmx.trigger(el, 'click');
#
Processing
// Process htmx attributes on dynamically added content
const newContent = document.createElement('div');
newContent.innerHTML = '<button hx-get="/data">Load</button>';
document.body.appendChild(newContent);
htmx.process(newContent);
#
Utilities
// Parse time strings
htmx.parseInterval('500ms'); // 500
htmx.parseInterval('2s'); // 2000
htmx.parseInterval('1m'); // 60000
// Get htmx version
console.log(htmx.version); // "1.9.10"
// Set logger for debugging
htmx.logger = function(elt, event, data) {
console.log(event, elt, data);
};
#
Extensions
// Define custom extension
htmx.defineExtension('my-extension', {
onEvent: function(name, event) {
if (name === 'htmx:configRequest') {
event.detail.headers['X-My-Header'] = 'value';
}
},
transformResponse: function(text, xhr, elt) {
return text.toUpperCase();
},
isInlineSwap: function(swapStyle) {
return swapStyle === 'my-swap';
},
handleSwap: function(swapStyle, target, fragment, settleInfo) {
// Custom swap logic
return true; // Return true if handled
}
});
// Remove extension
htmx.removeExtension('my-extension');
#
A.13 Configuration
#
Configuration Options
#
Setting Configuration
Via JavaScript:
htmx.config.historyCacheSize = 20;
htmx.config.defaultSwapDelay = 100;
htmx.config.timeout = 30000;
Via meta tag:
<meta name="htmx-config" content='{
"historyCacheSize": 20,
"defaultSwapDelay": 100,
"timeout": 30000,
"selfRequestsOnly": true
}'>
#
Common Configuration Patterns
<!-- Production security settings -->
<meta name="htmx-config" content='{
"selfRequestsOnly": true,
"allowEval": false,
"timeout": 30000
}'>
<!-- SPA-like history handling -->
<meta name="htmx-config" content='{
"historyCacheSize": 50,
"refreshOnHistoryMiss": false,
"scrollIntoViewOnBoost": true
}'>
<!-- Performance tuning -->
<meta name="htmx-config" content='{
"defaultSwapDelay": 0,
"defaultSettleDelay": 10,
"getCacheBusterParam": true
}'>
#
Quick Reference Card
#
Most-Used Attributes
#
Common Patterns
Debounced Search:
<input hx-get="/search"
hx-target="#results"
hx-trigger="keyup changed delay:300ms">
Inline Edit:
<button hx-get="/edit/5"
hx-target="closest tr"
hx-swap="outerHTML">
Edit
</button>
Modal Form:
<button hx-get="/form"
hx-target="#modal-container">
Open Form
</button>
Delete with Confirmation:
<button hx-delete="/items/5"
hx-target="closest tr"
hx-swap="delete"
hx-confirm="Delete this item?">
Delete
</button>
#
Key Response Headers
#
Essential CSS
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline; }
.htmx-request { opacity: 0.5; }
#
Key Events
For detailed examples and explanations, refer to the relevant chapters in the main text.