htmx in a Nutshell

htmx extends HTML’s built-in concept of hypermedia controls.

To understand htmx, let’s take a look at the two most widely known such controls: <a> (anchors, aka “links”) & <form>s.

Here is a basic, boring anchor tag:

<a href="/blog">Blog</a>

If you get past how pedestrian this link is, it’s actually a pretty amazing little bit of technology: when a user clicks on this link in a browser, the browser will issue an HTTP GET request to /blog. The browser then loads the HTML response returned by the server into the browser’s window.

Forms are a bit more complicated:

<form method="post" action="/register"> <label>Email: <input type="email"></label> <button type="submit">Submit</button> </form>

When a user submits this form (by, say, clicking on the “Submit” button) a browser will issue an HTTP POST request to /register. Again, the browser will load the HTML response to this request into the browser window.

The common pattern here is that the user performs an action, say, a click, and the browser issues an HTTP request to a server and then loads the response HTML into the browser’s window.

How htmx Extends This Idea

The core idea of htmx is to use a few custom attributes to generalize this idea:

  • Any element can issue an HTTP request
  • Any event can trigger that request
  • The response HTML can be placed anywhere in the DOM

This small extension turns out to dramatically boost the expressiveness of HTML.

Here is a sample htmx-powered button:

<button hx-post="/clicked" hx-trigger="click" hx-target="#output" hx-swap="outerHTML"> Click Me </button> <output id="output"></output>

The htmx attributes start with hx-. Let’s go through each one:

  • hx-post="/clicked" - this button should issue a POST request to the /clicked relative URL
  • hx-trigger="click" - it should issue the request when the button is clicked
  • hx-target="#output" - it should target the element with the id output with the HTML in the response to this request
  • hx-swap="outerHTML" - it should replace the element entirely with that html

These four attributes let you specify how and when this button should issue a request and where in the DOM the resulting HTML should be placed.

One thing to note: the hx-trigger here is redundant. If it was omitted htmx would use the default trigger event which, in the case of buttons, is a click.

One important thing to understand is that htmx expects HTML from the server. In this case the server would return a partial bit of HTML, say a <div>, to replace the button. What htmx does not expect is JSON.

Because htmx works in terms of HTML it follows the original web programming model, using Hypertext As The Engine Of Application State (HATEOAS).

This makes developing with htmx feel much more like traditional web development than most front-end libraries today.

Installing htmx

htmx is a single JavaScript file with no dependencies. No build step is required to use it.

Installing CDN

To install htmx as a vanilla JavaScript library via the jsdeliver CDN, add this in your <head> tag:

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0" integrity="sha384-BvJpBiO8Kh31EqtJe5DRIeWrHWnCGkwytKs9NKFi86Hhw96dEqdEMzZDeK9iEGTc" crossorigin="anonymous"></script>

Or, if you wish the unminified version (perhaps for debug reasons) use:

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.js" integrity="sha384-ESzWv77gBOAGtF3d7B8QiQ786cghRRsyhuYVVJGsrFGxwT0Dj1fLxReX9Ul6t4n9" crossorigin="anonymous"></script>

ES Module

To install htmx as a JavaScript ES Module via the jsdeliver CDN, add this in your <head> tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.esm.min.js" integrity="sha384-lIFlfAj002Phzxvrea858KMM3/dXelBFF5rq+Oh5lChznUWuVI0mabAEUnsRa/2W" crossorigin="anonymous"></script>

or, unminified:

<script type="module" src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.esm.js" integrity="sha384-fh3WoeSX2U60P2sV0M8Y6xvhTB5cpTb+AAF28P71BzGUZ/KI7QYrzKfwTFD/OCr6" crossorigin="anonymous"></script>

Downloading/Vendoring htmx

While a CDN is convenient, you should consider self-hosting in production.

  1. Download htmx.min.js
  2. Save it to your project (e.g., /js/htmx.min.js)
  3. Add this in your <head> tag:
<script src="/js/htmx.min.js"></script>

This is sometimes called “vendoring” htmx.

You can also download:

Installing htmx via NPM

You can install htmx via the standard

npm install htmx.org@4.0.0
import 'htmx.org';
import htmx from 'htmx.org'; // Now you can use htmx.ajax(), htmx.find(), etc.

Installing htmx + the core extensions: htmax.js

htmx has many useful extensions that add functionality to it.

If you want to install a distribution that ships with most of the useful extensions already installed so you don’t have to think about it, you can use the htmax.js distribution.

Migrating From htmx 2.x to 4.x

There are three major behavioral changes between htmx 2.x and 4.x:

  • In htmx 2.0 attribute inheritance is implicit by default while in 4.0 it is explicit by default
  • In htmx 2.0, 400 and 500 response codes are not swapped by default, whereas in htmx 4.0 these requests will be swapped
    • To restore the 2.0 behavior, you can set the htmx.config.noSwap setting to [204, 304, '4xx', '5xx']
  • In htmx 2.0, history used a local cache snapshot for history navigation, while in 4.0 it issues a request to the server to get the full page to restore
    • htmx includes an hx-history-cache extension if you wish to have a local cache. This extension integrates with Alpine.js and hx-live seamlessly.

Event names were also standardized/rationalized.

Upgrade Checker

To make upgrading easier, htmx 4 ships with a command-line upgrade tool that scans your templates and JS files for htmx 2 code that needs updating.

It checks for the following:

  • Removed attributes
  • Old event names
  • Attribute inheritance patterns
  • Extension changes, etc.

You can run the upgrade checker via npx:

npx htmx.org@4.0.0 upgrade-check -- ./path/to/project/root npx htmx.org@4.0.0 upgrade-check --ext .vue ./path/to/project/root

By default, the tool scans .html, .php, .js, .ts, .jinja, .jinja2, .j2, .erb, and .hbs files. You can add additional HTML-like file extensions via the --ext argument.

Output is file:line format, clickable in most editors.

For more details see What’s New in 4.0

Extension authors who need to port their extensions to htmx 4 can refer to Migrating Extensions to 4.0

Issuing Requests & Handling Responses

The crux of htmx is issuing HTTP request in response to events and then placing the response HTML into the document.

The core attributes for driving this behavior are:

These attributes can be placed on any element to tell that element to issue a request when triggered:

<button hx-get="/info"> Get Information </button>

As of htmx 4.0, you can also use the following alternative attributes:

<button hx-action="/info" hx-method="GET"> Get Information </button>

This is inspired by the syntax that forms use:

<form action="/info" method="GET"> <button>Get Information</button> </form>

Triggering Requests

By default, HTTP requests are triggered by the “natural” event of an element:

  • input, textarea & select are triggered on the change event
  • form is triggered on the submit event
  • everything else is triggered by the click event

If you want different behavior you can use the hx-trigger attribute to specify which event will cause the request.

Here is a div that issues an HTTP POST to /mouse_entered when a mouse enters it:

<div hx-post="/mouse_entered" hx-trigger="mouseenter"> Mouse Trap </div>

Trigger Modifiers

A trigger can also have additional modifiers that change its behavior.

For example, if you want a request to only happen once, you can use the once modifier for the trigger:

<div hx-post="/mouse_entered" hx-trigger="mouseenter once"> Mouse Trap </div>

Other modifiers you can use for triggers are:

ModifierDescription
changedonly issue a request if the value of the element has changed
delay:<time interval>wait the given amount of time (e.g. 1s) before issuing the request. If the event triggers again, the countdown is reset
throttle:<time interval>issue the first request immediately, then wait the given amount of time (e.g. 1s) before issuing another. If more events occur during that period, the last one triggers a request at its end
from:<selector>listen for the event on a different element. Accepts CSS and extended selectors, and is used for things like keyboard shortcuts. The selector is not re-evaluated if the page changes
target:<selector>only fire if event.target matches the given CSS selector. Useful when you listen on a container but only want events from certain children
preventcall event.preventDefault()
stopcall event.stopPropagation(). consume does the same thing
haltshorthand for prevent stop
capturelisten during the capture phase, from the top down, rather than the bubble phase
passivetell the browser that the handler will not call preventDefault(), so the browser can scroll without waiting for your code

Note that a selector with whitespace in from or target needs parentheses, for example from:(form input).

Multiple triggers can be specified by separating the triggers with a comma.

You can use triggers to implement many common UX patterns, such as Active Search:

<input type="text" name="q" placeholder="Search..." hx-query="/search" hx-trigger="input delay:500ms, keyup[key=='Enter']" hx-target="#search-results"> <div id="search-results"></div>

This input will issue a QUERY request 500 milliseconds after an input event occurs or when the enter key is pressed.

The input inserts the resulting HTML into the div with the id search-results. (Response handling is discussed below.)

Trigger Filters

In the example above, you may have noticed the square brackets after the keyup event name. This is a trigger filter.

Trigger filters allow you to place a filtering JavaScript expression after the event name that allows you to cancel the trigger if conditions are not met. To cancel the trigger, the expression should return false.

Here is an example that triggers only on a Shift-Click of the element

<div hx-get="/shift_clicked" hx-trigger="click[shiftKey]"> Shift Click Me </div>

Properties like shiftKey will be resolved against the triggering event first, then against the global scope.

The this symbol will be set to the current element.

Note that trigger filters require the use of eval(), so they should not be used with a strict Content Security Policy.

Special Events

htmx provides a few special events for use in hx-trigger:

  • load - fires once when the element is first loaded
  • revealed - fires once when an element first scrolls into the viewport
  • intersect - fires when an element enters the viewport. Add the once modifier to fire only once. This supports three additional options:
    • root:<selector> - a CSS selector of the root element for intersection
    • rootMargin:<margin> - a margin around the root element
    • threshold:<float> - a floating point number between 0.0 and 1.0, indicating what amount of intersection to fire the event on

You can also use custom events to trigger requests. Dispatch them with htmx.trigger() or from the server with the HX-Trigger response header.

Polling

Polling is a simple technique where a web page periodically issues a request to the server to see if any updates have occurred. It is a simple mechanism for getting updated content from a server. It does not require a permanent server connection, and it tolerates network failures well.

In htmx you can implement polling via the every syntax in the hx-trigger attribute:

<div hx-get="/news" hx-trigger="every 2s"></div>

This tells htmx:

Every 2 seconds, issue a GET to /news and load the response into the div

Load Polling

Another technique that can be used to achieve polling in htmx is “load polling”, where an element specifies a load trigger along with a delay, and replaces itself with the response:

<div hx-get="/messages" hx-trigger="load delay:1s" hx-swap="outerHTML"> </div>

If the /messages end point keeps returning a div set up this way, it will keep “polling” back to the URL every second.

Load polling can be useful in situations where a poll has an end point at which point the polling terminates, such as when you are showing the user a progress bar.

Polling is a simple, useful technique for many problems but isn’t appropriate for more interactive situations. For these situations, consider streaming HTML.

Handling Responses

htmx expects the responses to the HTTP requests it makes to be HTML. This is in contrast with front-end frameworks like React, which use JSON-formatted responses instead.

Typically, htmx responses will be HTML fragments, that is small bits of HTML rather than a full document:

<ul id="contacts"> <li>Joe Blow</li> <li>Jane Doe</li> </ul>

Htmx will then swap this content into the document. To do this it needs two things:

  • A target - where to place the content
  • A swap strategy - how to place the content

The two attributes that control this are hx-target and hx-swap.

Targeting Elements

By default, responses target the element that made the request. You can change this by using the hx-target attribute, which takes a CSS selector that specifies the element to target:

<button hx-get="/info" hx-target="#output"> </button> <output id="output">-</output>
Targeting With Extended Selectors

In addition to plain selectors, htmx has the notion of extended selector syntax. This syntax increases the expressiveness of attributes like hx-target:

SelectorMatchesExample
<CSS selector>The same elements querySelectorAll() returns.hx-target="#results"
thisThe element itself (the default target)hx-target="this"
closest <CSS selector>The nearest ancestor that matches.hx-target="closest .card"
find <CSS selector>The first descendant that matches.hx-target="find .username"
nextThe next sibling element.hx-target="next"
next <CSS selector>The first match after this element in document order.hx-target="next .results"
previousThe previous sibling element.hx-target="previous"
previous <CSS selector>The first match before this element in document order.hx-target="previous .results"
bodyThe document body.hx-target="body"
documentThe document.hx-trigger="click from:document"
windowThe window.hx-trigger="scroll from:window"
hostThe shadow root host. Useful inside a web component.hx-target="host"

Relative selectors are particularly useful for cleaning up your DOM:

<button hx-get="/info" hx-target="next output"> </button> <output>-</output>

Here we use the next relative selector to target the next output element without requiring an ID. This technique can be particularly useful when you have repeated content such as a table and want to avoid generating IDs to make targets work out properly.

Configuring Swaps

The hx-swap attribute controls how the content is swapped into (or in place of) the target element.

The default swapping mechanism is innerHTML: htmx places the response content inside the target element. It does not replace it. This is in line with the way that HTML’s native iframes work.

Other swapping algorithms available are:

NameDescription
innerHTMLthe default, puts the content inside the target element
outerHTMLreplaces the entire target element with the returned content
outerSyncmorphs the target’s attributes, then replaces its children. The target stays in the DOM
before (or beforebegin)prepends the content before the target in the target’s parent element
prepend (or afterbegin)prepends the content before the first child inside the target
append (or beforeend)appends the content after the last child inside the target
after (or afterend)appends the content after the target in the target’s parent element
deletedeletes the target element regardless of the response
nonedoes not append content from response (Out-of-Band Swaps and Response Headers will still be processed)
innerMorphmorphs the children of the target element, preserving as much of the existing DOM as possible
outerMorphmorphs the target element itself, preserving as much of the existing DOM as possible
textContentSet the target’s text content (no HTML parsing)

As an example, afterend inserts the response after the target, rather than inside it. This is how a “load more” button appends new rows and then replaces itself:

<tr id="row-20"> <td>Row 20</td> <td> <button hx-get="/rows?page=2" hx-target="closest tr" hx-swap="outerHTML"> Load More </button> </td> </tr>
Preserving Content During Swaps

Some elements must survive a swap untouched, such as a playing video or a third party widget. Add hx-preserve to keep an element as it is when an ancestor is replaced:

<div id="results"> <video id="player" hx-preserve></video> </div>

Preserved elements match by id. Give the element a stable id, and include the same id in the response.

Morphing Swaps

In htmx 4 there are now built-in innerMorph and outerMorph swaps. Previously, morphing swaps were available only via the Idiomorph extension.

Morph swaps merge new content into the existing DOM rather than simply replacing it, attempting to preserve existing nodes in the DOM.

Morphing rather than replacing content can do a better job preserving things like focus, video state, etc. by mutating existing nodes in-place during a swap operation.

See the Morphing Guide for more information on using this technique.

Swap Options

The hx-swap attribute also supports options for tuning the swapping behavior of htmx.

For example, by default htmx will swap in the title of any title tag found in the response content.

You can turn this behavior off by setting the ignoreTitle modifier to true:

<button hx-post="/like" hx-swap="outerHTML ignoreTitle:true">Like</button>

The modifiers available on hx-swap are:

OptionDescription
swapA time interval (e.g., 100ms, 1s) to delay the swap operation
settleA time interval to delay the settle phase, which runs after the swap. Defaults to 1ms
transitiontrue or false, whether to use the view transition API for this swap
ignoreTitleIf set to true, any title found in the new content will be ignored and not update the document title
striptrue or false, whether to strip the outer element when swapping (unwrap the content)
focusScrolltrue or false, whether to scroll focused elements into view
swapEmptytrue or false, whether to perform the main swap when the response body is empty (false skips it).
scrolltop or bottom, will scroll the target element to its top or bottom
showtop or bottom, will scroll the target element’s top or bottom into view
targetA selector to retarget the swap to a different element

All swap modifiers appear after the swap style.

See the hx-swap documentation for more details on these options.

Selecting Response Content

Sometimes you may want to only swap a sub-element within the content returned by the server. A common case is a server that can only render full pages, when you only need one part of that page.

The hx-select attribute takes a plain CSS selector and extracts only that content for swapping:

<button hx-get="/info" hx-target="#result" hx-select="#info-detail"> Get Info </button> <div id="result"></div>

Selecting “Out of Band” Content

If you want to make additional swaps from a larger piece of server content you can use the hx-select-oob attribute to do so:

<button hx-get="/dashboard" hx-target="#main" hx-select="#main-content" hx-select-oob="#alert,#sidebar"> Refresh </button>

Here, in addition to filtering the main swap down to the element with the id main, htmx finds elements with the ids alert and sidebar in the response content and swaps each one over the element with the same id in the current page.

Forms

Working with forms and inputs in htmx is natural if you are used to regular HTML.

Input Values

By default, an element that causes a request will include its value if it has one.

If the element is a form it will include the values of all inputs within it.

If an element issues a request that sends a body (that is, anything except GET and DELETE), the values of all the inputs of the associated form will be included (typically this is the nearest enclosing form, but could be different if, for example, the form attribute is used).

<form> <input name="email" value="joe@example.com"> <input name="plan" value="pro"> <button hx-post="/signup">Sign Up</button> </form>

The button issues a POST to /signup with email=joe@example.com&plan=pro.

Including Other Values

If you want to include the values of other elements in a request, you can use the hx-include attribute.

The hx-include attribute takes an extended CSS selector and will include the values of all matching elements.

To send computed values (rather than values held in inputs) use the hx-vals attribute.

<button hx-post="/save" hx-vals='{"draft": true}'>Save Draft</button>

You can use a js: prefix to compute the value dynamically when the request is made:

<button hx-post="/save" hx-vals='js:{scrollY: window.scrollY}'>Save</button>

File Uploads

By default, htmx uses the standard application/x-www-form-urlencoded format for submitting values to the server.

If you wish to upload files via an htmx request you can set the hx-encoding attribute to multipart/form-data.

This will use a FormData object to submit the request, which will properly include the file in the request.

Note that, depending on what you are using on the server side to process requests, you may have to handle requests with this body type very differently.

See file upload pattern for a complete example.

Form Validation

htmx integrates with the HTML5 Validation API and will not issue a request for a form if a validatable input is invalid.

Non-form elements do not validate before they make requests by default, but you can enable validation by setting the hx-validate attribute on them to “true”.

Attribute Inheritance

Changes in htmx 4.0

In htmx 2.0 attribute inheritance was implicit by default: elements inherited the attributes on their parents, such as hx-target. In htmx 4.0 attribute inheritance is now explicit by default, using the :inherited modifier.

Attribute inheritance allows you to “hoist” attributes up the DOM, in order to avoid code duplication.

Consider the following HTML:

<button hx-delete="/account" hx-confirm="Are you sure?"> Delete My Account </button> <button hx-put="/account" hx-confirm="Are you sure?"> Update My Account </button>

Here we have a duplicate hx-confirm attribute.

We can hoist this attribute to a parent element using the :inherited modifier on the attribute:

<div hx-confirm:inherited="Are you sure?"> <button hx-delete="/account"> Delete My Account </button> <button hx-put="/account"> Update My Account </button> </div>

This hx-confirm attribute will now apply to all htmx-powered elements within it.

Adding To An Inherited Value

By default, an attribute on a child overrides an inherited value for that attribute. You can use the :append modifier to append the child value to the inherited value instead:

<div hx-vals:inherited="tenant:acme"> ... <button hx-post="/save" hx-vals:append="source:save-btn">Save</button> </div>

If no ancestor provides a value, the append value is used on its own.

You can combine :inherited and :append on a element if you want child elements to inherit the new value.

Multi-Target Updates

htmx requests normally update one target element. Sometimes you need to update multiple parts of the page at once.

For example, after submitting a form, you might want to update both the form itself and a notification counter elsewhere on the page.

htmx provides two mechanisms to update multiple targets from a single response:

  • Out-of-Band Swaps
  • Partial Tags (new in htmx 4)

Out-of-Band Swaps

Out-of-Band (OOB) swaps allow you to swap a single element from the position that it is located in a response to another position in the DOM, based on the elements id.

To do this, you can add hx-swap-oob="true" to an element in your response.

When you do this, htmx will find the element already in the DOM with the same id in your page and swap it.

So, if this content was returned by the server in response to submitting a form:

<div id="message" hx-swap-oob="true"> Form submitted successfully! </div> <form id="my-form"> <!-- Updated form content --> </form>

Then the form element will be swapped into the DOM the normal way, according to the hx-target and hx-swap attributes, but the div will be swaped “out of band” and replace the element in the DOM with the id message, elsewhere in the page.

Customizing OOB Swaps

You can control the swap style of an OOB swap by setting the value of hx-swap-oob to a valid swap strategy:

<div id="notifications" hx-swap-oob="beforeend"> <span>New notification</span> </div>

This appends the content to div#notifications instead of replacing it.

Pure OOB Responses

htmx removes the OOB elements from the response before the main swap. If nothing is left, the main swap is skipped and the main target keeps its content.

<!-- Server returns only OOB content: the main target is left untouched --> <div id="notifications" hx-swap-oob="true"> <span class="badge">5</span> </div>

Two settings change this. Set htmx.config.allowEmptySwapAfterOOB to true to run the main swap anyway, everywhere:

<meta name="htmx-config" content="allowEmptySwapAfterOOB:true">

Or set the swapEmpty modifier on one element, which wins over the config:

<button hx-post="/submit" hx-swap="outerHTML swapEmpty:true">Submit</button>

Note that <hx-partial> elements always skip an empty main swap, and neither setting changes that. See Pure <hx-partial> Responses.

Partials (<hx-partial>)

The hx-partial tag is new in htmx 4, and it addresses issues that have come up in our experience with OOB swaps over the years.

To use the hx-partial tag, simply wrap content in it.

You can then use the normal htmx attributes to specify exactly how to swap that content into the DOM:

<hx-partial hx-target="#messages" hx-swap="beforeend"> <div class="message">New message content</div> </hx-partial> <hx-partial hx-target="#notifications"> <span class="badge">5</span> </hx-partial> <form id="my-form"> <!-- Main form content --> </form>

<hx-partial> tags can have the following attributes:

  • hx-target - CSS selector for where to place content
  • id - Shorthand alternative to hx-target. Targets the element with that ID (e.g. <hx-partial id="messages"> targets #messages)
  • hx-swap - Optional. Swap style (defaults to innerHTML)

Some server-side template languages remove tags they do not know. For these, use the equivalent <template> form:

<template hx type="partial" hx-target="#messages" hx-swap="beforeend"> <div class="message">New message content</div> </template>

Pure <hx-partial> Responses

When a response contains only <hx-partial> elements and no main content, htmx does not perform the main swap: it assumes you only want to do partial replacement with the response.

<!-- Server returns only partials: the main target is left untouched --> <hx-partial hx-target="#notifications"> <span class="badge">5</span> </hx-partial> <hx-partial hx-target="#messages" hx-swap="beforeend"> <div class="message">New message</div> </hx-partial>

If you want the main target cleared, add swapEmpty:true to hx-swap on the triggering element:

<button hx-post="/submit" hx-swap="outerHTML swapEmpty:true">Submit</button>

OOB vs <hx-partial>

OOB swaps were designed for simple one-for-one replacements in the DOM. They make sense when you have one element that you want to replace directly with another single element.

Partials are more general but correspondingly more complicated. They make sense when you want to replace arbitrary content (not just content keyed by id) with any form of content. Because the replacement content is within the <hx-partial> tag, it can be arbitrarily complex (e.g. multiple top level elements)

Both approaches can be used within a single response if desired.

Synchronizing Requests

Often you want to coordinate the requests between two elements. For example, you may want a request from one element to supersede the request of another element, or to wait until the other element’s request has finished.

htmx offers a hx-sync attribute to help you accomplish this.

Consider a race condition between a form submission and an individual input’s validation request in this HTML:

<form hx-post="/store"> <input id="title" name="title" type="text" hx-post="/validate" hx-trigger="change"> <button type="submit">Submit</button> </form>

Without using hx-sync, filling out the input and immediately submitting the form triggers two parallel requests to /validate and /store.

Using hx-sync="closest form" on the input and hx-sync="this:replace" on the form will watch for requests from the form and abort an input’s in flight request:

<form hx-post="/store" hx-sync="this:replace"> <input id="title" name="title" type="text" hx-post="/validate" hx-trigger="change" hx-sync="closest form"> <button type="submit">Submit</button> </form>

This resolves the synchronization between the two elements in a declarative way.

Aborting A Request

htmx also supports a programmatic way to cancel requests: you can send the htmx:abort event to an element to cancel any in-flight requests:

<button id="request-button" hx-post="/example"> Issue Request </button> <button onclick="htmx.trigger('#request-button', 'htmx:abort')"> Cancel Request </button>

More examples and details can be found on the hx-sync attribute page.

Request Indicators

When an HTTP request is issued by htmx it is often good to let the user know that something is happening.

You can accomplish this in htmx by using the special htmx-indicator class.

The htmx-indicator class is defined by htmx such that the opacity of any element with this class is 0 by default, making it invisible but present in the DOM.

When htmx issues a request, it will add a htmx-request class onto an element (either the requesting element or another element, if specified).

The htmx-request class will cause a child element with the htmx-indicator class on it to transition to an opacity of 1 which shows the indicator.

<button hx-get="/click"> Click Me! <img class="htmx-indicator" src="/spinner.gif" alt="Loading..."> </button>

When this button makes a request the htmx-request class will be added to it.

This will reveal the spinner GIF element inside of it.

Custom Request Indicator CSS

The htmx-indicator class uses opacity to hide and show the progress indicator but if you would prefer another mechanism you can create your own CSS transition like so:

.htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: inline; } .htmx-request.htmx-indicator { display: inline; }

Targeting A Specific Indicator

If you want the htmx-request class added to a different element, you can use the hx-indicator attribute with an extended CSS selector to do so:

<div> <button hx-get="/click" hx-indicator="#indicator"> Click Me! </button> <img id="indicator" class="htmx-indicator" src="/spinner.gif" alt="Loading..."/> </div>

Disabling Elements

Another common need is to disable elements while a request is in flight to prevent the user from interacting with them.

You can add the disabled attribute to elements for the duration of a request by using the hx-disable attribute:

<button hx-post="/submit" hx-disable="this">Submit</button>

The value is an extended selector, so you can disable other elements too. Here the whole fieldset is disabled while the request is in flight:

<fieldset> <input name="email"> <button hx-post="/submit" hx-disable="closest fieldset">Submit</button> </fieldset>

User Confirmations

Often you will want to confirm an action before issuing a request. htmx supports the hx-confirm attribute, which allows you to confirm an action using a simple javascript dialog:

<button hx-delete="/account" hx-confirm="Are you sure you wish to delete your account?"> Delete My Account </button>

hx-confirm may also contain JavaScript by using the js: or javascript: prefix. In this case the JavaScript will be evaluated and, if a promise is returned, it will wait until the promise resolves with a true value to continue

<script> async function swalConfirm() { let result = await Swal.fire({ title: "Are you sure?", text: "You won't be able to revert this!", icon: "warning", showCancelButton: true, confirmButtonColor: "#3085d6", cancelButtonColor: "#d33", confirmButtonText: "Yes, delete it!" }) return result.isConfirmed } </script> <button hx-delete="/account" hx-confirm="js:swalConfirm()"> Delete My Account </button>

Swapping Animations

There are two different ways to animate elements when htmx swaps a response into the DOM:

  • CSS Transitions
  • View Transitions

Note that animations, while visually interesting, should never detract from usability and should generally be less than 100 milliseconds in duration.

CSS Transitions

CSS Transitions are a well established mechanism for animating content in the DOM.

htmx will ensure that any content with a stable ID will have CSS transitions applied when a swap occurs, regardless of what swapping approach you use.

So, if this original content:

<div id="div1">Original Content</div>

is replaced with this new content:

<div id="div1" class="red">New Content</div>

You can write a CSS transition between the two like so:

.red { color: red; transition: all ease-in 100ms; }

View Transitions

A newer animation technique is the View Transitions API, which gives developers a way to create a structured animated transition between different DOM states.

View Transitions are much more sophisticated (and complicated!) than CSS transitions but give you much more control over the animation.

htmx supports view transitions via the following:

  • Setting htmx.config.transitions to true globally will enable view transitions for all swaps
  • Per-swap via the hx-swap attribute transition option: hx-swap="outerHTML transition:true"
  • For boosted elements via the transition option: hx-boost="transition:true"

Note that the default view transition is a 250 millisecond cross-fade which, in our opinion, is a very bad default for swapping, so you will want to override this if you use view transitions with htmx.

In htmx you can “boos” regular HTML anchors and forms using the hx-boost attribute.

This attribute will convert anchor tags and forms into fecth()-based requests that, by default, target the body of the page.

Here is an example:

<div hx-boost:inherited="true"> <a href="/blog">Blog</a> <a href="/about">About</a> <a href="/contact">Contact</a> </div>

The anchor tags in this div will issue an AJAX GET request to /blog and swap the response into the body tag.

Note that hx-boost is using the inherited modifier here.

Advantages & Disadvantages of Boosting

Boosting is a feature that has been part of htmx since it was called intercooler. In the olden days there were big advantages to it:

  • It eliminated the Flash of Unstyled Content (FOUC)
  • It enabled CSS transitions between pages
  • It removed the need to reparse CSS/JS between pages
  • It allowed the preservation of elements with the hx-preserve

Over time, browsers have gotten better at inter-page transitions, eliminating the FOUC via paint holding and making View Transitions work for full-page navigation.

This has reduced the advantages of boosting. There is still a performance benefit to boosting, and it is still the only way to use CSS transitions & element preservation on navigation, however.

A disadvantage that people sometimes run into (which is one of the reasons it is faster) is that boosted elements to not reset the JavaScript environment. With normal navigation, the browser completely resets the JavaScript environment.

When boosting you have to be careful to not redefine things on accident, which can lead to JavaScript errors.

Generally, boosting is controversial in the htmx community. Some people love it, some people discourage it.

For what it’s worth, we use boosting in this documentation website.

Boosting & Progressive Enhancement

A nice feature of hx-boost is that it degrades gracefully if JavaScript is not enabled: the links and forms continue to work, they simply don’t use ajax requests.

This is known as Progressive Enhancement, and it allows a wider audience to use your site’s functionality.

Other htmx patterns can be adapted to achieve progressive enhancement as well, but they will require more thought.

Consider the active search example. As it is written, it will not degrade gracefully: someone who does not have javascript enabled will not be able to use this feature. This is done for simplicity’s sake, to keep the example as brief as possible.

However, you could wrap the htmx-enhanced input in a form element:

<form action="/search" method="POST"> <input class="form-control" type="search" name="search" placeholder="Begin typing to search users..." hx-query="/search" hx-trigger="keyup changed delay:500ms, search" hx-target="#search-results" hx-indicator=".htmx-indicator"> </form>

With this in place, javascript-enabled clients would still get the nice active-search UX, but non-javascript enabled clients would be able to hit the enter key and still search.

Even better, you could add a “Search” button as well. You would then need to update the form with an hx-post that mirrored the action attribute, or perhaps use hx-boost on it.

You would need to check on the server side for the HX-Request header to differentiate between an htmx-driven and a regular request, to determine exactly what to render to the client.

Other patterns can be adapted similarly to achieve the progressive enhancement needs of your application.

As you can see, this requires more thought and more work. It also rules some functionality entirely out of bounds. These tradeoffs must be made by you, the developer, with respect to your projects goals and audience.

Accessibility

Accessibility is a concept closely related to progressive enhancement. Using progressive enhancement techniques such as hx-boost will make your htmx application more accessible to a wide array of users.

htmx-based applications are very similar to normal, non-fetch() driven web applications because htmx is HTML-oriented.

As such, the normal HTML accessibility recommendations apply. For example:

  • Use semantic HTML as much as possible (i.e. the right tags for the right things)
  • Ensure focus state is clearly visible
  • Associate text labels with all form fields
  • Maximize the readability of your application with appropriate fonts, contrast, etc.

Browser History Support

Changes in htmx 4.0

History support in htmx 4.0 has changed significantly. We no longer snapshot the DOM and keep a copy in sessionStorage.

Instead, we issue a full page request every time someone navigates to a history element. This is much less error-prone and foolproof. It also eliminates security concerns regarding keeping history state in accessible storage

This change makes history restoration much more reliable and reduces client-side complexity.

Htmx provides a simple mechanism for interacting with the browser history API:

If you want a given element to push its request URL into the browser navigation bar and add the current state of the page to the browser’s history, include the hx-push-url attribute:

<a hx-get="/blog" hx-push-url="true">Blog</a>

When a user clicks on this link, htmx will push a new location onto the history stack.

When a user hits the back button, htmx will retrieve the old content from the original URL and swap it back into the body, simulating “going back” to the previous state.

NOTE: If you push a URL into the history, you must be able to navigate to that URL and get a full page back! A user could copy and paste the URL into an email, or new tab.

Replacing The Current URL

If you want to chante the URL without updating history use the hx-replace-url attribute instead:

<a hx-get="/account" hx-replace-url="true">My Account</a>

History Response Headers

The server can override either attribute for a single response with the HX-Push-Url and HX-Replace-Url response headers.

Restoring Only Part Of The Page

By default htmx replaces the whole body when a user navigates back or forward.

If you wish for history to be restored only within a specific element you can use the hx-history-elt attribute:

<body> <nav><!-- never replaced --></nav> <main hx-history-elt> <h1>Page 1</h1> </main> </body>

On a history navigation htmx requests the URL, selects the hx-history-elt element out of the response, and swaps it over the current one, leaving the rest of the page untouched.

Configuring History

The htmx.config.history setting allow you to specify how history works:

ValueBehavior
truethe default. htmx requests the URL and swaps the response
"reload"htmx does a full page reload instead of a request
falsehtmx does not handle history at all. The browser behaves normally

If you want the htmx 2.x behavior of restoring history from a local snapshot instead of a full server request, use the hx-history-cache extension.

Advanced Request & Response Techniques

The out-of-the-box request & response behavior of htmx is often sufficient for people, but some times you may want to do more advanced HTTP handling. This section documents how to do so.

HTTP Response Code Handling

By default, htmx will swap all responses it receives into the DOM except for responses with the HTTP response codes 204 or 304.

If you respond with a 204 - No Content response code, and htmx will ignore the content of the response and not swap anything, even if the response has a body.

If the response code is 400 or 500, htmx will trigger an htmx:response:error event.

Configuring Response Code Handling

You can customize this behavior using the hx-status attribute, which takes a response code pattern after a colon:

htmx tests the exact code first, then the two-digit wildcard (e.g. 50x), then the one-digit wildcard (e.g. 5xx).

Here is an example:

<form hx-post="/submit" hx-target="#result" hx-status:422="target:#validation-errors" hx-status:5xx="target:#server-error" hx-status:503="swap:none"> <input name="email"> <button type="submit">Submit</button> </form> <div id="result"></div> <div id="validation-errors"></div> <div id="server-error"></div>

This tells htmx:

  • Successful responses (2xx) swap into #result (default behavior)
  • 422 responses swap into #validation-errors
  • 503 responses do not swap at all
  • 5xx all other 500 responses swap into #server-error

You can also use the htmx.config.noSwap configuration for global configuration of response code handling.

For example, to revert to the htmx 2.0 behavior of not swapping on 4xx and 5xx response codes you can add the following configuration:

<meta name="htmx-config" content='{"noSwap": [204, 304, "4xx", "5xx"]}'>

Request Headers

htmx includes headers in the requests it makes:

HeaderDescription
HX-Boostedindicates that the request is via an element using hx-boost
HX-Current-URLthe current URL of the browser
HX-Requestalways “true”
HX-Request-Type"partial" for targeted swaps, "full" for body-level or hx-select requests
HX-Sourcethe source element in tag#id format (e.g. button#submit)
HX-Targetthe target element in tag#id format (e.g. div#results)

htmx also sends HX-History-Restore-Request when it refetches a page after a miss in the history cache. See Browser History Support for more info.

Adding Your Own Headers

To add headers to a request, use the hx-headers attribute.

<div hx-get="/data" hx-headers='{"X-Widget-Id": "42"}'>Get Data</div>

You can use a js: prefix to compute the headers when the request is made.

<div hx-get="/data" hx-headers='js:{"X-Scroll": window.scrollY}'>Get Data</div>

Here is an example that sends a CSRF token on every htmx request.

<body hx-headers:inherited='js:{"X-CSRF-Token": getCsrfToken()}'> ... </body>

If you want to set headers programmatically, use the htmx:config:request event.

Response Headers

htmx supports the following response headers:

HeaderDescription
HX-Locationallows you to do a client-side redirect that does not do a full page reload
HX-Push-Urlpushes a new url into the history stack
HX-Redirectcan be used to do a client-side redirect to a new location
HX-Refreshif set to “true” the client-side will do a full refresh of the page
HX-Replace-Urlreplaces the current URL in the location bar
HX-Reswapallows you to specify how the response will be swapped. See hx-swap for possible values
HX-Retargeta CSS selector that updates the target of the content update to a different element on the page
HX-Reselecta CSS selector that allows you to choose which part of the response is used to be swapped in. Overrides an existing hx-select on the triggering element
HX-Triggerallows you to trigger client-side events

The HX-Trigger Response Headers can be particularly useful, allowing you to trigger client-side JavaScript code from the server.

Per-Request Configuration With hx-config

The hx-config attribute allows you to control fine-grained details of the request issued by htmx:

<button hx-post="/api/users" hx-config="timeout:5s">Create User</button>

Most of hx-config options map directly onto the Fetch API request options:

OptionDescription
timeoutaborts the request after this time. Accepts 500ms, 5s, 2m, or a number of milliseconds. Defaults to htmx.config.defaultTimeout
credentials"omit", "same-origin" or "include". Defaults to "same-origin"
cachea fetch cache mode, such as "no-cache" or "reload"
redirect"follow", "error" or "manual"
referrera referrer URL, or "no-referrer"
integritya subresource integrity value
validatetrue to validate the form before htmx sends the request. See Form Validation

The mode Option Is Not Available

hx-config does not allow the mode option for security reasons.

htmx always resets mode to htmx.config.mode, which defaults to "same-origin".

This stops an attacker who can inject an attribute from widening the scope of a request.

See Security Considerations for more info.

Streaming HTML

For more interactive scenarios, where a server sends multiple updates to the DOM from a single request, htmx provides various streaming HTML extensions.

The streaming extensions provided by htmx use:

SSE

Server-Sent Events let one HTTP response stream multiple events to the browser over a single connection.

The hx-sse extension supports swapping content via these events.

Consider the following button:

<button hx-post="/generate" hx-target="next output" hx-swap="append"> Generate </button> <output></output>

With the htmx SSE extension installed, as unnamed events are received from the server the content in those events will be appended to the output tag. This allows for a natural, streaming mechanism for inserting content as it becomes available into an element.

For more details, see the hx-sse extension documentation.

Web Sockets

In contrast with SSE, Web Sockets hold a connection open in both directions, so the server and the browser can both send messages at any time.

The hx-ws extension supports swapping content from these messages as well as sending messages to the server from DOM elements.

Consider the following chat window:

<div hx-ws:connect="/chat" hx-target="#messages" hx-swap="append"> <div id="messages"></div> <form hx-ws:send> <input name="message"> <button>Send</button> </form> </div>

With the htmx Web Socket extension installed, the connection opens on load and every message the server sends is appended to #messages. The form sends its values back over the same connection as JSON, so no new request is made.

For more details, see the hx-ws extension documentation.

Multi-Part

A multipart/mixed response carries many parts in one body, with a delimiter chosen by the server:

HTTP/1.1 200 OK Content-Type: multipart/mixed; boundary=... Hello --... Content-Type: text/html , world!

The hx-multipart extension supports swapping the content of these parts into the DOM as they arrive.

Consider the generate button we looked at in the SSE example:

<button hx-post="/generate" hx-target="next output" hx-swap="append"> Generate </button> <output></output>

With the htmx multi-part extension installed, if the server responds with a request of type multipart/mixed;, as parts are received from the server the content in those parts will be appended to the output tag.

For more details, see the hx-multipart extension documentation.

Picking A Streaming Technology

Each streaming technology has strengths and weaknesses.

SSE is widely supported and is our default recommendation for streaming responses.

Web Sockets are the most complicated but support bi-directional communication.

Multi-part, despite being very old, is less widely supported by server side frameworks. However, we feel it more naturally follows HTTP semantics. We recommend it if you are a purist and are willing to do a bit of work on the server side to make this style of response easy to work with.

Client-Side Scripting

htmx encourages a hypermedia-based approach to building web applications, with requests to servers updating content with response HTML.

However, for modern web applications it is often desirable to add client-side scripting to your website in order to improve interactivity.

htmx offers various tools to help make this easier:

  • A rich set of events that it triggers
  • A scripting API against the htmx object
  • hx-on attributes for basic inline scripting
  • Support for Alpine.js for more advanced inline scripting
  • The hx-live extension as a (new in htmx 4) DOM-oriented alternative to Alpine.js

Events

Htmx has an extensive set of events that you can listen for to log or modify behaviors with:

document.body.addEventListener('htmx:after:init', function (evt) { setUpElement(evt.detail.elt); });

Here, we are using vanilla JavaScript to listen for an element being initialized by htmx and applying some additional logic to it with our own custom setUpElement() function.

See htmx Events Guide for more details on using htmx events effectively.

The htmx Object API

The global htmx JavaScript object has the following methods available on it:

MethodDescription
htmx.ajax()issues an htmx request from JavaScript
htmx.find()finds the first element that matches a CSS selector
htmx.findAll()finds all elements that match a CSS selector
htmx.process()initializes htmx attributes on an element and its descendants
htmx.swap()runs the swap lifecycle without a request
htmx.initialize()initializes htmx manually, if the automatic startup is too early
htmx.on()adds an event listener
htmx.onLoad()runs a callback when htmx processes new content
htmx.trigger()dispatches a custom event on an element
htmx.registerExtension()registers an htmx extension
htmx.parseInterval()converts a time string such as 5s to milliseconds
htmx.timeout()creates a promise that resolves after a time interval

The htmx object also holds htmx.config, which sets the global configuration.

See the methods reference for the signature and examples of each method.

The hx-on:* Attributes

You can embed JavaScript event handlers directly on elements by using the hx-on:<event name> syntax:

<button hx-on:click="alert('You clicked me!'); await timeout(1000); console.log('done')"> Click Me! </button>

hx-on attributes have the following top level symbols available:

SymbolDescription
thisthe element that holds the hx-on attribute
eventthe event that triggered the handler
event detailsevery property of event.detail is in scope directly. For htmx events this includes ctx, the request context
htmx methodsevery method of the htmx object is in scope without the htmx. prefix (e.g. find()

Note that this feature requires eval() and thus may not work if you have a strict CSP.

Alpine.js Support

Alpine.js is a very popular JavaScript library that adds significant expressivity to inline scripting:

  • Event handlers with x-on
  • Reactive state with x-data
  • Conditional content with x-show and x-if
  • Two-way form binding with x-model
  • Helpers such as $refs, $store, $dispatch and $watch

Alpine is a very popular library among htmx developers. Out of the box the two technologies play together very well, but there is an hx-alpine-compat extension that smooths over some corner cases when integrating the two libraries.

hx-live

hx-live, new in htmx 4, is our own take on DOM-oriented, reactive scripting for the web. It is inspired by Alpine, jQuery and hyperscript.

hx-live provides a jQuery-like q() selector function that allows you to select one or many elements and update/mutate them as a group. This function is made available at the top level in hx-on: attributes, and supports relative selectors.

<input placeholder="Enter your name" type="text"> <button hx-on:click="this.text = 'Hello, ' + q('previous input').value"></button>

When you click this button it will update its text based on the value of the preceding input.

hx-live also supports DOM-based reactivity:

<input placeholder="Enter your name" type="text"> <p :text="'Hello, ' + q('previous input').value"></p>

In this case, the paragraph will update as you enter text into the input.

For more details, see the hx-live Programmers Guide and the hx-live extension reference.

Other 3rd Party JavaScript

htmx is designed to integrate well with most third party JavaScript libraries.

If the library fires events on the DOM, you can use those events to trigger requests from htmx.

A good example of this is the SortableJS demo:

<form class="sortable" hx-post="/items" hx-trigger="end"> <div class="htmx-indicator">Updating...</div> <div><input type='hidden' name='item' value='1'/>Item 1</div> <div><input type='hidden' name='item' value='2'/>Item 2</div> <div><input type='hidden' name='item' value='2'/>Item 3</div> </form>

With Sortable, as with most javascript libraries, you need to initialize content at some point.

In htmx, the cleanest way to do this is using the htmx.onLoad() method to register a callback.

This callback will be called whenever htmx inserts new content into the DOM, allowing you to initialize any widgets in the new content.

htmx.onLoad((content) => { var sortables = content.querySelectorAll(".sortable"); for (var i = 0; i < sortables.length; i++) { var sortable = sortables[i]; new Sortable(sortable, { animation: 150, ghostClass: 'blue-background-class' }); } })

This will ensure that as new content is added to the DOM by htmx, sortable elements are properly initialized.

Hyperscript

The experimental hyperscript scripting language is a sister project of htmx and integrates seamlessly with it.

Definitely not for everyone, but a pretty fun little language:

<button _="on click add .highlight to <p/> in me">

Web Components

Note that htmx doesn’t automatically initialize content inside web components: you must manually initialize it by calling htmx.process in the connectedCallback() method:

customElements.define('my-counter', class extends HTMLElement { connectedCallback() { const shadow = this.attachShadow({mode: 'open'}) shadow.innerHTML = ` <button hx-post="/increment" hx-target="#count">+1</button> <div id="count">0</div> ` htmx.process(shadow) // Initialize htmx for this shadow DOM } })

Note that this is true regardless of whether or not the component uses a Shadow DOM.

Targeting Elements Outside Shadow DOM

If you are using the Shadow DOM in a component, selectors like hx-target will only see elements inside that same Shadow DOM.

To break out of a components Shadow DOM and target the Web Component itself you can use host as the target:

<!-- Inside a Web Component --> <button hx-get="..." hx-target="host"> ... </button>

To break out of the shadow DOM and target an element in the broader DOM, you can use the global keyword, followed by a space and the selector:

<!-- Inside a Web Component --> <button hx-get="..." hx-target="global #target"> ... </button>

Extensions

htmx supports extensions to augment its core hypermedia infrastructure.

The following extensions ship with htmx:

ExtensionCategoryDescription
hx-multipartStreaming HTMLStream HTML with multipart/mixed
hx-sseStreaming HTMLStream HTML with text/event-stream (SSE)
hx-wsStreaming HTMLStream HTML and send data over WebSockets
hx-browser-indicatorUXShow tab’s spinner with hx-browser-indicator
hx-liveUXOur own DOM-based reactive scripting solution
hx-pendingUXShow custom content during requests
hx-promptUXPrompt before requests with hx-prompt='Reason?'
hx-preloadPerformancePreload on hover with hx-preload='mouseover'
hx-history-cachePerformanceRestore back/forward pages from sessionStorage
hx-ptagPerformanceSkip unchanged polls with HX-PTag: "v42"
hx-downloadSwapsDownload files with hx-swap='download'
hx-headSwapsMerge <head> tags with hx-head='merge'
hx-targetsSwapsTarget many elements with hx-targets='.selector'
hx-upsertSwapsUpdate or insert elements with hx-swap='upsert'
htmx-2-compatCompatibilityRestore htmx 2.x defaults and event names on htmx 4
hx-alpine-compatCompatibilityRun htmx alongside Alpine.js without conflicts
hx-cspSecurityMake htmx work under strict Content Security Policy

Note that many these extensions are come pre-bundled into htmax.js as a single file.

Using Extensions

To install an extension, include the extension script after htmx is included.

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0/dist/ext/hx-sse.js"></script>

Or with a bundler:

import 'htmx.org'; import 'htmx.org/dist/ext/hx-sse';

Building Extensions

If you wish to build your own htmx extension, see htmx Extension Authoring Guide.

Security Considerations

htmx allows you to define logic directly in your DOM. This has a number of advantages, the largest being Locality of Behavior, which makes your system easier to understand and maintain.

A concern with this approach, however, is security: since htmx increases the expressiveness of HTML, if a malicious user is able to inject HTML into your application, they can leverage this expressiveness of htmx to malicious ends.

Rule 1: Escape All User Content

The first rule of HTML-based web development has always been: do not trust input from the user. You should escape all 3rd party, untrusted content that is injected into your site. This is to prevent, among other issues, XSS attacks.

There is extensive documentation on XSS and how to prevent it on the excellent OWASP Website, including a Cross Site Scripting Prevention Cheat Sheet.

The good news is that this is a very old and well understood topic, and the vast majority of server-side templating languages support automatic escaping of content to prevent just such an issue.

That being said, there are times people choose to inject HTML more dangerously, often via some sort of raw() mechanism in their templating language. This can be done for good reasons, but if the content being injected is coming from a 3rd party then it must be scrubbed, including removing attributes starting with hx- and data-hx, as well as inline <script> tags, etc.

If you are injecting raw HTML and doing your own escaping, a best practice is to whitelist the attributes and tags you allow, rather than to blacklist the ones you disallow.

htmx Security Tools

Of course, bugs happen and developers are not perfect, so it is good to have a layered approach to security for your web application, and htmx provides tools to help secure your application as well.

Let’s take a look at them.

hx-ignore

The first tool htmx provides to help further secure your application is the hx-ignore attribute. This attribute will prevent processing of all htmx attributes on a given element, and on all elements within it. So, for example, if you were including raw HTML content in a template (again, this is not recommended!) then you could place a div around the content with the hx-ignore attribute on it:

<div hx-ignore> <%= raw(user_content) %> </div>

And htmx will not process any htmx-related attributes or features found in that content. This attribute cannot be disabled by injecting further content: if an hx-ignore attribute is found anywhere in the parent hierarchy of an element, it will not be processed by htmx.

CSP Options

Browsers also provide tools for further securing your web application. The most powerful tool available is a Content Security Policy. Using a CSP you can tell the browser to, for example, not issue requests to non-origin hosts, to not evaluate inline script tags, etc.

CSP can be set via an HTTP header or a <meta> tag. HTTP headers are preferred, <meta> tags do not enforce all directives and scripts that appear before the <meta> tag in the document are not covered by it:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<nonce>'

A full discussion of CSPs is beyond the scope of this document, but the MDN Article provides a good jumping-off point for exploring this topic.

Controlling Cross-Origin Requests

htmx defaults htmx.config.mode to "same-origin", which causes the browser to reject any cross-origin fetch, even if an attacker injects an hx-get pointing elsewhere.

This setting is enforced for markup: any mode value in a per-element hx-config attribute is ignored and reset to the global config value. Injected markup like hx-config='{"mode":"cors"}' cannot widen request scope.

If your application legitimately needs CORS (e.g. an API on a different subdomain):

  1. Set the mode globally:
    htmx.config.mode = "cors";
  2. Lock down reachable origins with connect-src:
    <meta http-equiv="Content-Security-Policy" content="connect-src 'self' https://api.example.com">

With both in place, htmx can reach your API but injected target URLs to other origins are blocked by CSP.

hx-csp Extension

For sites using CSP script nonces, the hx-csp extension provides deep integration:

  • Gates all htmx attribute processing behind a per-request nonce, blocking injected htmx attributes
  • Automatically creates a 'htmx' Trusted Types policy so only htmx can write HTML into DOM sinks
  • Replaces new Function() eval with nonce-based script injection when safeEval:true is set, removing the need for unsafe-eval

See the hx-csp extension docs for full setup instructions.

htmx & Eval

htmx uses new Function() for some optional features:

  • Event filters
  • The hx-on attribute
  • Attribute values starting with js: or javascript:

All of these are optional. If you don’t use them you can omit unsafe-eval from your CSP entirely.

If you do use these features, the hx-csp extension with safeEval:true replaces new Function() with nonce-based script injection, enabling them without unsafe-eval.

CSP & Inline Styles

htmx injects its indicator CSS using Constructable Stylesheets (document.adoptedStyleSheets), which are not subject to style-src CSP restrictions.

The one area to be aware of is morph swaps when used alongside JS frameworks like Alpine that set style attributes via JavaScript. During morph, htmx reconciles attributes between the old and new element, including any style value. Under a strict style-src policy without 'unsafe-inline', this setAttribute("style", ...) call will produce a CSP violation.

Add "style" to morphIgnore to skip it:

<meta name="htmx-config" content='{"morphIgnore":["data-htmx-powered","style"]}'>

Class-based CSS transitions continue to work normally.

CSRF Prevention

The assignment and checking of CSRF tokens are typically backend responsibilities, but htmx can support returning the CSRF token automatically with every request using the hx-headers attribute. The attribute needs to be added to the element issuing the request or one of its ancestor elements. This makes the html and body elements effective global vehicles for adding the CSRF token to the HTTP request header, as illustrated below.

<html lang="en" hx-headers:inherited='{"X-CSRF-TOKEN": "CSRF_TOKEN_INSERTED_HERE"}'> : </html>

The above elements are usually unique in an HTML document and should be easy to locate within templates.

Caching

htmx works with standard HTTP caching mechanisms out of the box.

If your server adds the Last-Modified HTTP response header to the response for a given URL, the browser will automatically add the If-Modified-Since request HTTP header to the next requests to the same URL.

For polling use cases where you want the server to skip responses when content hasn’t changed, see the ptag extension.

Be mindful that if your server can render different content for the same URL depending on some other headers, you need to use the Vary response HTTP header.

For example, if your server renders the full HTML when the HX-Request header is missing or false, and it renders a fragment of that HTML when HX-Request: true, you need to add Vary: HX-Request. That causes the cache to be keyed based on a composite of the response URL and the HX-Request request header rather than being based just on the response URL.

Debugging

Declarative and event driven programming with htmx (or any other declarative language) can be a wonderful and highly productive activity, but one disadvantage when compared with imperative approaches is that it can be trickier to debug.

Figuring out why something isn’t happening, for example, can be difficult if you don’t know the tricks.

Here are some tips:

Errors and warnings flow to console.error / console.warn by default. To also see every event htmx dispatches, set htmx.config.logAll = true:

htmx.config.logAll = true;

Observability tools (Sentry, DataDog RUM, LogRocket, etc.) capture console.* automatically, so htmx logs flow into your existing pipeline without any extra setup.

Of course, that won’t tell you why htmx isn’t doing something. You might also not know what events a DOM element is firing to use as a trigger. To address this, you can use the monitorEvents() method available in the browser console:

monitorEvents(htmx.find("#theElement"));

This will spit out all events that are occurring on the element with the id theElement to the console, and allow you to see exactly what is going on with it.

Note that this only works from the console, you cannot embed it in a script tag on your page.

Finally, push come shove, you might want to just debug htmx.js by loading up the unminimized version.

You would most likely want to set a break point in the methods to see what’s going on.

And always feel free to jump on the Discord if you need help.

Editor Support

While htmx is simple enough that editor support is not necessary to use it, see Editor Support for information on tooling available in your preferred editor.

Configuration

Htmx has configuration options that can be accessed either programmatically or declaratively.

They are listed below:

Config VariableInfo
htmx.config.logAlldefaults to false, if set to true htmx will log all events to the console for debugging
htmx.config.prefixdefaults to "data-hx-", a secondary attribute prefix recognised alongside the always-active hx- prefix (e.g. data-hx-get works by default). Set to "" to disable. Must be set via meta tag, setting this after page load will not apply correctly.
htmx.config.transitionsdefaults to false, whether to use view transitions when swapping content (if browser supports it)
htmx.config.historydefaults to true, whether to enable history support. Set to "reload" to do a full page reload on history navigation instead of an AJAX request
htmx.config.modedefaults to 'same-origin', the fetch mode for AJAX requests. Can be 'cors', 'no-cors', or 'same-origin'
htmx.config.defaultSwapdefaults to innerHTML
htmx.config.indicatorClassdefaults to htmx-indicator
htmx.config.requestClassdefaults to htmx-request
htmx.config.includeIndicatorCSSdefaults to true (determines if the indicator styles are loaded)
htmx.config.defaultTimeoutdefaults to 60000 (60 seconds), the number of milliseconds a request can take before automatically being terminated
htmx.config.inlineScriptNoncedefaults to unset, meaning that no nonce will be added to inline scripts
htmx.config.extensionsdefaults to '', a comma-separated list of extension names to load (e.g., 'preload,pending')
htmx.config.morphIgnoredefaults to ["data-htmx-powered"], array of attribute name prefixes to preserve when morphing elements
htmx.config.morphScanLimitlimits the number of nodes scanned during morphing
htmx.config.morphSkipdefaults to '[hx-morph-skip]', CSS selector for elements to completely skip during morphing (they stay frozen)
htmx.config.morphSkipChildrendefaults to '[hx-morph-skip-children]', CSS selector for elements whose attributes update but children are preserved during morphing
htmx.config.noSwapdefaults to [204, 304], array of HTTP status codes that should not trigger a swap
htmx.config.allowEmptySwapAfterOOBdefaults to false, whether the main swap still runs when a response contained only out-of-band elements
htmx.config.implicitInheritancedefaults to false, if set to true attributes will be inherited from parent elements automatically without requiring the :inherited modifier
htmx.config.defaultFocusScrolldefaults to false, whether to scroll focused elements into view after swap
htmx.config.defaultSettleDelaydefaults to 1 (ms), delay between swap and settle phases
htmx.config.metaCharacterdefaults to undefined, allows you to use a custom character instead of : for attribute modifiers (e.g., - to use hx-get-inherited instead of hx-get:inherited)

You can set most options directly in JavaScript, or you can use a meta tag (accepts HCON or JSON):

Note: Some options are read only once during initialisation and must be set via the meta tag to take effect. These include prefix, extensions, and metaCharacter.

<meta name="htmx-config" content='{"defaultSwap":"innerHTML"}'>

Conclusion

And that’s it!

Have fun with htmx!

You can accomplish quite a bit without writing a lot of code!