Skip to content

Htmx.TagHelpers (v1.x)

Install the Htmx.TagHelpers NuGet package to your ASP.NET Core project. Targets .NET Core 3.1+ projects.

Terminal window
dotnet add package Htmx.TagHelpers

Make the Tag Helpers available in your project by adding the following line to your _ViewImports.cshtml:

@addTagHelper *, Htmx.TagHelpers

You’ll generally need URL paths pointing back to your ASP.NET Core backend. Htmx.TagHelpers mimics the URL generation built into ASP.NET Core, so you link HTMX to your routes the same way you’d use asp-* helpers.

<div hx-target="this">
<button hx-get
hx-page="Index"
hx-page-handler="Snippet"
hx-swap="outerHtml">
Click Me (Razor Page w/ Handler)
</button>
</div>
<div hx-target="this">
<button hx-get
hx-controller="Home"
hx-action="Index"
hx-route-id="1">
Click Me (Controller)
</button>
</div>
<div hx-target="this">
<button hx-post
hx-route="named">
Click Me (Named)
</button>
</div>

The URL tag helper supports the following routing attributes:

Attribute Description
hx-action The name of the action method.
hx-controller The name of the controller.
hx-area The name of the area.
hx-page The name of the Razor Page.
hx-page-handler The name of the Razor Page handler.
hx-route Name of the route (mutually exclusive with action/controller/page).
hx-route-{key} Additional route parameter values (e.g., hx-route-id="1").
hx-protocol The protocol for the URL (e.g., "https").
hx-host The host name.
hx-fragment The URL fragment name.

These work with any of the htmx method attributes: hx-get, hx-post, hx-put, hx-patch, and hx-delete.

The HtmxHeadersTagHelper lets you build the hx-headers JSON attribute from individual prefixed attributes, which can be cleaner than writing JSON by hand:

<button hx-post="/api/data"
hx-headers-X-Custom-Header="my-value"
hx-headers-Authorization="Bearer token123">
Submit
</button>

This produces:

<button hx-post="/api/data" hx-headers='{"X-Custom-Header":"my-value","Authorization":"Bearer token123"}'>
Submit
</button>

An additional htmx-config tag helper is included that can be applied to a meta element in your page’s head that makes creating HTMX configuration simpler. For example, below we can set the historyCacheSize, default indicatorClass, and whether to include ASP.NET Core’s anti-forgery tokens as an additional element on the HTMX configuration.

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="htmx-config"
historyCacheSize="20"
indicatorClass="htmx-indicator"
includeAspNetAntiforgeryToken="true"
/>
<!-- additional elements... -->
</head>

The resulting HTML will be.

<!DOCTYPE html>
<html lang="en">
<head>
<meta name="htmx-config" content='{"indicatorClass":"htmx-indicator","historyCacheSize":20,"antiForgery":{"formFieldName":"__RequestVerificationToken","headerName":"RequestVerificationToken","requestToken":"<token>"}}' />
<!-- additional elements... -->
</head>

The config tag helper supports the full set of htmx configuration options:

Attribute Type Description
historyEnabled bool Whether htmx history is enabled.
historyCacheSize int Number of history entries to cache (default: 10).
refreshOnHistoryMiss bool Issue full page refresh on history misses.
defaultSwapStyle string Default swap style (default: "innerHTML").
defaultSwapDelay int Default swap delay in milliseconds (default: 0).
defaultSettleDelay int Default settle delay in milliseconds (default: 100).
includeIndicatorStyles bool Whether indicator styles are loaded.
indicatorClass string CSS class for indicators (default: "htmx-indicator").
requestClass string CSS class applied during requests (default: "htmx-request").
settlingClass string CSS class applied during settling (default: "htmx-settling").
swappingClass string CSS class applied during swapping (default: "htmx-swapping").
addedClass string CSS class applied to newly added elements (default: "htmx-added").
selfRequestsOnly bool Only allow requests to the same domain.
allowScriptTags bool Whether script tags are processed (default: true).
allowEval bool Whether eval is allowed (default: true).
useTemplateFragments bool Use HTML template tags for parsing.
wsReconnectDelay string WebSocket reconnect delay strategy (default: "full-jitter").
wsBinaryType string WebSocket binary type (default: "blob").
disableSelector string CSS selector for elements to skip htmx processing.
timeout int Default timeout for HTTP requests in milliseconds.
scrollBehavior string Scroll behavior (default: "smooth").
defaultFocusScroll bool Whether to scroll focused elements into view.
getCacheBusterParam bool Add cache-buster param to GET requests.
globalViewTransitions bool Enable global view transitions.
ignoreTitle bool If true, htmx won’t update the document title.
scrollIntoViewOnBoost bool Scroll boosted targets into viewport.
disableInheritance bool Completely disable attribute inheritance.
inlineScriptNonce string CSP nonce for inline scripts.
inlineStyleNonce string CSP nonce for inline styles.
withCredentials bool Whether to send credentials with AJAX requests.
allowNestedOobSwaps bool Process out-of-band swaps on nested elements.
includeAspNetAntiforgeryToken bool Include ASP.NET Core antiforgery token in the config.

You can set the attribute includeAspNetAntiforgeryToken on the htmx-config element. Then you’ll need to include this additional JavaScript in your web application. We include the attribute __htmx_antiforgery to track the event listener was added already. This keeps us from accidentally re-registering the event listener.

if (!document.body.attributes.__htmx_antiforgery) {
document.addEventListener("htmx:configRequest", evt => {
let httpVerb = evt.detail.verb.toUpperCase();
if (httpVerb === 'GET') return;
let antiForgery = htmx.config.antiForgery;
if (antiForgery) {
// already specified on form, short circuit
if (evt.detail.parameters[antiForgery.formFieldName])
return;
if (antiForgery.headerName) {
evt.detail.headers[antiForgery.headerName]
= antiForgery.requestToken;
} else {
evt.detail.parameters[antiForgery.formFieldName]
= antiForgery.requestToken;
}
}
});
document.addEventListener("htmx:afterOnLoad", evt => {
if (evt.detail.boosted) {
const parser = new DOMParser();
const html = parser.parseFromString(evt.detail.xhr.responseText, 'text/html');
const selector = 'meta[name=htmx-config]';
const config = html.querySelector(selector);
if (config) {
const current = document.querySelector(selector);
// only change the anti-forgery token
const key = 'antiForgery';
htmx.config[key] = JSON.parse(config.attributes['content'].value)[key];
// update DOM (usually unnecessary)
current.replaceWith(config);
}
}
});
document.body.attributes.__htmx_antiforgery = true;
}

You can access the snippet in two ways. The first is to use the HtmxSnippets static class in your views.

<script>
@Html.Raw(HtmxSnippets.AntiforgeryJavaScript)
</script>

A simpler way is to use the HtmlExtensions class that extends IHtmlHelper.

@Html.HtmxAntiforgeryScript()

This html helper will result in a <script> tag along with the previously mentioned JavaScript. Note: You can still register multiple event handlers for htmx:configRequest, so having more than one is ok.

Note that if the hx-[get|post|put] attribute is on a <form ..> tag and the <form> element has a method="post" (and also an empty or missing action="") attribute, the ASP.NET Tag Helpers will add the Anti-forgery Token as an input element and you do not need to further configure your requests as above. You could also use hx-include pointing to a form, but this all comes down to a matter of preference.

Additionally, and the recommended approach is to use the HtmxAntiforgeryScriptEndpoint, which will let you map the JavaScript file to a specific endpoint, and by default it will be /_htmx/antiforgery.js.

app.UseAuthorization();
// registered here
app.MapHtmxAntiforgeryScript();
app.MapRazorPages();
app.MapControllers();

You can also pass a custom path and choose between minified or non-minified output:

app.MapHtmxAntiforgeryScript("/custom/path.js", minified: false);

You can now configure this endpoint with caching, authentication, etc. More importantly, you can use the script in your head tag now by applying the defer tag, which is preferred to having JavaScript at the end of a body element.

<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta
name="htmx-config"
historyCacheSize="20"
indicatorClass="htmx-indicator"
includeAspNetAntiforgeryToken="true"/>
<title>@ViewData["Title"] - Htmx.Sample</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<script src="~/lib/jquery/dist/jquery.min.js" defer></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="https://unpkg.com/htmx.org@@1.9.2" defer></script>
<!-- this uses the static value in a script tag -->
<script src="@HtmxAntiforgeryScriptEndpoints.Path" defer></script>
</head>