htmx/www/content/docs.md
2025-10-28 18:07:14 +02:00

73 KiB
Raw Blame History

+++ title = "Documentation" [extra] custom_classes = "wide-content" +++

htmx in a Nutshell

htmx is a library that allows you to access modern browser features directly from HTML, rather than using javascript.

To understand htmx, first let's take a look at an anchor tag:

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

This anchor tag tells a browser:

"When a user clicks on this link, issue an HTTP GET request to '/blog' and load the response content into the browser window".

With that in mind, consider the following bit of HTML:

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML">
    Click Me!
</button>

This tells htmx:

"When a user clicks on this button, issue an HTTP POST request to '/clicked' and use the content from the response to replace the element with the id parent-div in the DOM"

htmx extends and generalizes the core idea of HTML as a hypertext, opening up many more possibilities directly within the language:

  • Any element can issue an HTTP request
  • Any event can trigger requests
  • Any HTTP verb can be used
  • Any element can be the target for update by the request

Note that when you are using htmx the server typically responds with HTML, not JSON.

htmx follows the original web programming model of the web, using Hypertext As The Engine Of Application State without users really needing to understand that concept: just return HTML.

2.x to 4.x Migration Guide

Version 2 (and Version 1) of htmx are still supported, but the latest version of htmx is 4.x.

If you are migrating to htmx 4.x from htmx 2.x, please see the htmx 2.x migration guide.

Installing

htmx is a dependency-free, browser-oriented javascript library. This means that using it is as simple as adding a <script> tag to your document head.

There is no need for a build system to use htmx.

Via A CDN (e.g. jsDelivr)

The fastest way to get going with htmx is to load it via a CDN. You can simply add this to your head tag and get going:

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

An unminified version is also available as well:

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

While the CDN approach is extremely simple, you may want to consider not using CDNs in production.

Download a copy

The next easiest way to install htmx is to simply copy it into your project.

Download htmx.min.js from jsDelivr and add it to the appropriate directory in your project and include it where necessary with a <script> tag:

<script src="/path/to/htmx.min.js"></script>

npm

For npm-style build systems, you can install htmx via npm:

npm install htmx.org@4.0.0-alpha1

After installing, youll need to use appropriate tooling to use node_modules/htmx.org/dist/htmx.js (or .min.js). For example, you might bundle htmx with some extensions and project-specific code.

Webpack

If you are using webpack to manage your javascript:

  • Install htmx via your favourite package manager (like npm or yarn)
  • Add the import to your index.js
import 'htmx.org';

If you want to use the global htmx variable (recommended), you need to inject it to the window scope:

  • Create a custom JS file
  • Import this file to your index.js (below the import from step 2)
import 'path/to/my_custom.js';
  • Then add this code to the file:
window.htmx = require('htmx.org');
  • Finally, rebuild your bundle

AJAX

The core of htmx are two attributes that allow you to issue fetch()-based AJAX requests directly from HTML:

Attribute Description
hx-action Specifies a URL to issue the request to
hx-method Specifies the HTTP Method to use

They can be used like so:

<button hx-method="post" hx-action="/messages">
    Post To Messages
</button>

This tells the browser:

When a user clicks on this button, issue a POST request to the URL /messages and load the response into the button

If no method is specified, the default GET method will be used.

Because it is so common to specify a method and action together, htmx provides five attributes that allow you to specify both in the same single attribute.

Attribute Description
hx-get Issues a GET request to the given URL
hx-post Issues a POST request to the given URL
hx-put Issues a PUT request to the given URL
hx-patch Issues a PATCH request to the given URL
hx-delete Issues a DELETE request to the given URL

These attributes are typically used in place of hx-method and hx-action.

Here is the example above redone using hx-post:

<button hx-post="/messages">
    Post To Messages
</button>

Triggering Requests

By default 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 posts 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 a few 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:

  • changed - only 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> - wait the given amount of time (e.g. 1s) before issuing the request. Unlike delay if a new event occurs before the time limit is hit the event will be discarded, so the request will trigger at the end of the time period.
  • from:<CSS Selector> - listen for the event on a different element. This can be used for things like keyboard shortcuts. Note that this CSS selector is not re-evaluated if the page changes.

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

<input type="text" name="q"
       hx-get="/search"
       hx-trigger="keyup changed delay:500ms"
       hx-target="#search-results"
       placeholder="Search...">
<div id="search-results"></div>

This input will issue a request 500 milliseconds after a key up event if the input has been changed and inserts the results into the div with the id search-results.

Multiple triggers can be specified in the hx-trigger attribute, separated by commas.

Trigger Filters

You may also apply trigger filters by using square brackets after the event name, enclosing a javascript expression that will be evaluated.

If the expression evaluates to true the event will trigger, otherwise it will not.

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.

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 once when an element first intersects the viewport. This supports two additional options:
    • root:<selector> - a CSS selector of the root element for intersection
    • 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 if you have an advanced use case.

Polling

If you want an element to poll the given URL rather than wait for an event, you can use the every syntax with 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.

Request Indicators

When an AJAX request is issued it is often good to let the user know that something is happening since the browser will not give them any feedback. You can accomplish this in htmx by using htmx-indicator class.

The htmx-indicator class is defined so 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 put 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, showing the indicator.

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

Here we have a button. When it is clicked the htmx-request class will be added to it, which will reveal the spinner gif element. (I like SVG spinners these days.)

While the htmx-indicator class uses opacity to hide and show the progress indicator, 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;
}

If you want the htmx-request class added to a different element, you can use the hx-indicator attribute with a 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>

Here we call out the indicator explicitly by id. Note that we could have placed the class on the parent div as well and had the same effect.

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

Targets

If you want the response to be loaded into a different element other than the one that made the request, you can use the hx-target attribute, which takes a CSS selector.

Looking back at our Live Search example:

<input type="text" name="q"
    hx-get="/trigger_delay"
    hx-trigger="keyup delay:500ms changed"
    hx-target="#search-results"
    placeholder="Search...">
<div id="search-results"></div>

You can see that the results from the search are going to be loaded into the element with the id search-results, rather than into the input tag itself.

Extended CSS Selectors

hx-target, and most attributes that take a CSS selector, support an "extended" CSS syntax:

  • You can use the this keyword, which indicates that the element that the hx-target attribute is on is the target
  • The closest <CSS selector> syntax will find the closest ancestor element or itself, that matches the given CSS selector. (e.g. closest tr will target the closest table row to the element)
  • The next <CSS selector> syntax will find the next element in the DOM matching the given CSS selector.
  • The previous <CSS selector> syntax will find the previous element in the DOM matching the given CSS selector.
  • find <CSS selector> which will find the first child descendant element that matches the given CSS selector. (e.g find tr would target the first child descendant row to the element)

In addition, a CSS selector may be wrapped in < and /> characters, mimicking the query literal syntax of hyperscript.

Relative targets like this can be useful for creating flexible user interfaces without peppering your DOM with lots of id attributes.

Swapping

htmx offers a few different ways to swap the HTML returned into the DOM. By default, the content replaces the the target element, which is called an outerHTML swap.

You can modify this by using the hx-swap attribute with any of the following values:

Name Description
outerHTML the default, replaces the entire target element with the returned content
innerHTML puts the content inside the target element
afterbegin prepends the content before the first child inside the target
beforebegin prepends the content before the target in the target's parent element
beforeend appends the content after the last child inside the target
afterend appends the content after the target in the target's parent element
delete deletes the target element regardless of the response
none does not append content from response (Out of Band Swaps and Response Headers will still be processed)

Morph Swaps

In addition to the standard swap mechanisms above, htmx also supports morphing swaps, via extensions. Morphing swaps attempt to merge new content into the existing DOM, rather than simply replacing it. They often do a better job preserving things like focus, video state, etc. by mutating existing nodes in-place during the swap operation, at the cost of more CPU.

TODO: are we going to have a morph swap out of the box

View Transitions

The View Transitions API gives developers a way to create an animated transition between different DOM states.

By default, htmx uses the viewTransition() API when swapping in content.

View Transitions can be configured using CSS, as outlined in the Chrome documentation for the feature.

TODO: more docs on this

You can see a view transition example on the Animation Examples page.

Swap Options

The hx-swap attribute supports many options for tuning the swapping behavior of htmx. For example, by default htmx will swap in the title of a title tag found anywhere in the new 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:

Option Description
transition true or false, whether to use the view transition API for this swap
ignoreTitle If set to true, any title found in the new content will be ignored and not update the document title
scroll top or bottom, will scroll the target element to its top or bottom
show top or bottom, will scroll the target element's top or bottom into view

All swap modifiers appear after the swap style is specified, and are colon-separated.

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

Synchronization

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:abort" on the input will watch for requests on the form and abort the input's request if a form request is present or starts while the input request is in flight:

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

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

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.

CSS Transitions

TODO: are we going to support CSS transitions?

Server Actions

// TODO: do

Out of Band Swaps

// TODO: are we going to deprecate OOB in favor of server actions?

If you want to swap content from a response directly into the DOM by using the id attribute you can use the hx-swap-oob attribute in the response html:

<div id="message" hx-swap-oob="true">Swap me directly!</div>
Additional Content

In this response, div#message would be swapped directly into the matching DOM element, while the additional content would be swapped into the target in the normal manner.

You can use this technique to "piggy-back" updates on other requests.

Troublesome Tables

Table elements can be problematic when combined with out of band swaps, because, by the HTML spec, many can't stand on their own in the DOM (e.g. <tr> or <td>).

To avoid this issue you can use a template tag to encapsulate these elements:

<template>
  <tr id="message" hx-swap-oob="true"><td>Joe</td><td>Smith</td></tr>
</template>

Selecting Content To Swap

If you want to select a subset of the response HTML to swap into the target, you can use the hx-select attribute, which takes a CSS selector and selects the matching elements from the response.

TODO need to implement You can also pick out pieces of content for an out-of-band swap by using the hx-select-oob attribute, which takes a list of element IDs to pick out and swap.

Preserving Content During A Swap

If there is content that you wish to be preserved across swaps (e.g. a video player that you wish to remain playing even if a swap occurs) you can use the hx-preserve attribute on the elements you wish to be preserved.

Parameters

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.

As with HTML forms, the name attribute of the input is used as the parameter name in the request that htmx sends.

TODO: this may change

Additionally, if the element causes a non-GET request, 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 e.g. <button form="associated-form"> is used).

If you wish to include the values of other elements, you can use the hx-include attribute with a CSS selector of all the elements whose values you want to include in the request.

TODO fix link Finally, if you want to programmatically modify the parameters, you can use the htmx:config:request event.

File Upload

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 your server-side technology, you may have to handle requests with this type of body content very differently.

See the examples section for more advanced form patterns, including progress bars and error handling.

Confirming Requests

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>

Attribute Inheritance

Unlike htmx 2, in htmx 4 attribute inheritance is explicit, rather than implicit.

Inheritance allows you to "hoist" attributes up the DOM to avoid code duplication.

Consider the following htmx:

<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:

<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.

Boosting

Htmx supports "boosting" regular HTML anchors and forms with the hx-boost attribute. This attribute will convert all anchor tags and forms into AJAX 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.

Progressive Enhancement

A 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 simplicitys 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-post="/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 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-AJAX 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.

SSE

Unlike htmx 2, htmx 4 has built-in support for Server-Sent Events (SSE).

Use the typical hx-get, hx-post, hx-put, hx-patch, or hx-delete attributes. When the server responds with Content-Type: text/event-stream instead of Content-Type: text/html, htmx automatically handles the stream.

Each SSE message with a data: line (and no event: line) is processed like a regular htmx response, respecting hx-target, hx-select, and hx-swap attributes.

Like fetch-event-source, htmx's custom SSE implementation supports request bodies, custom headers, and all HTTP methods (not just GET), and Page Visibility API integration (using the pauseHidden modifier).

Basic Usage

<button hx-get="/stream" hx-target="#llm-output" hx-swap="innerHTML">
    Stream LLM Response
</button>

<div id="llm-output"></div>

The server sends SSE messages with data: lines:

data: H

data: He

// ...

data: Hello partner!

Each message replaces the target element's content. The stream processes until the connection closes, then stops. No reconnection occurs by default.

Stream Modes

The hx-stream attribute controls reconnection behavior. The default mode is once, so it doesn't need to be specified.

  • once (default): Process stream until connection closes. No reconnection.
  • continuous: Reconnect automatically if connection drops. Retries with exponential backoff.
<body hx-get="/updates" hx-stream="continuous" hx-trigger="load">
    ...
</body>

Note: hx-stream="continuous" is primarily intended for use with <htmx-action type="partial"> to enable real-time updates to multiple parts of the page via a permanently open SSE connection.

Partial Updates

Use <htmx-action type="partial"> tags to update multiple targets from a single stream, similar to hx-swap-oob.

<button hx-get="/activity" hx-stream="continuous">
    Start Activity Stream
</button>

<div id="metrics">0</div>
<div id="status">Idle</div>

Server sends multiple partials in one message:

data: <htmx-action type="partial" hx-target="#metrics">42</htmx-action>

data: <htmx-action type="partial" hx-target="#status">Active</htmx-action>

Custom Events

SSE event: lines trigger custom DOM events. When an event: line is present, htmx fires that event instead of performing a normal swap. Use this for lightweight updates without swapping DOM elements.

<button hx-get="/progress"
        hx-on:progress="htmx.find('#bar').style.width = event.detail.data + '%'">
    Start
</button>

Server sends custom events:

event: progress
data: 50

event: progress
data: 100

Configuration

Global stream config in htmx.config.streams:

<meta name="htmx:config" content='{
  "streams": {
    "mode": "once",
    "maxRetries": 3,
    "initialDelay": 500,
    "maxDelay": 30000,
    "pauseHidden": false
  }
}'>
  • mode: 'once' or 'continuous'
  • maxRetries: Maximum reconnection attempts (default: Infinity)
  • initialDelay: First reconnect delay in ms (default: 500)
  • maxDelay: Max backoff delay in ms (default: 30000)
  • pauseHidden: Pause stream when page is hidden (default: false). Uses the Page Visibility API to pause the stream when the browser window is minimized or the tab is in the background.

You can override these settings per-element using the hx-stream attribute:

<button hx-get="/stream"
        hx-stream="continuous maxRetries:10 initialDelay:1s pauseHidden:true">
    Start
</button>

Events

  • htmx:before:sse:stream: Fired before processing stream
  • htmx:before:sse:message: Fired before each message swap. Cancel with event.detail.message.cancelled = true
  • htmx:after:sse:message: Fired after each message swap
  • htmx:after:sse:stream: Fired when stream ends
  • htmx:before:sse:reconnect: Fired before reconnection attempt. Cancel with event.detail.reconnect.cancelled = true

Web Sockets

Web Sockets are supported via and extensions. Please see the WebSocket extension page to learn more.

History Support

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.

An ajax request with the HX-History-Restore-Request set to true, and expects back the HTML needed for the entire page.

// TODO - need to revisit if this is an issue still You should always set htmx.config.historyRestoreAsHxRequest to false to prevent the HX-Request header which can then be safely used to respond with partials. // END TODO - need to revisit if this is an issue still

Alternatively, if the htmx.config.refreshOnHistoryMiss config variable is set to true, it will issue a hard browser refresh.

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.

Additionally, htmx will need the entire page when restoring history if the page.

Requests & Responses

Htmx expects responses to the AJAX requests it makes to be HTML, typically HTML fragments (although a full HTML document, matched with a hx-select tag can be useful too).

Htmx will then swap the returned HTML into the document at the target specified and with the swap strategy specified.

Sometimes you might want to do nothing in the swap, but still perhaps trigger a client side event (see below).

For this situation, by default, you can return a 204 - No Content response code, and htmx will ignore the content of the response.

TODO fix link In the event of a connection error, the htmx:error event will be triggered.

Configuring Response Handling

TODO: need to figure out how we are gonna handle this (telroshan)

CORS

TODO: update for fetch()

When using htmx in a cross origin context, remember to configure your web server to set Access-Control headers in order for htmx headers to be visible on the client side.

See all the request and response headers that htmx implements.

Request Headers

htmx includes a number of useful headers in requests:

// TODO which are we going to keep?

Header Description
HX-Boosted indicates that the request is via an element using hx-boost
HX-History-Restore-Request "true" if the request is for history restoration after a miss in the local history cache
HX-Request always "true" except on history restore requests if `htmx.config.historyRestoreAsHxRequest' disabled

Response Headers

// TODO which are we going to keep?

htmx supports some htmx-specific response headers:

  • HX-Location - allows you to do a client-side redirect that does not do a full page reload
  • HX-Push-Url - pushes a new url into the history stack
  • HX-Redirect - can be used to do a client-side redirect to a new location
  • HX-Refresh - if set to "true" the client-side will do a full refresh of the page
  • HX-Replace-Url - replaces the current URL in the location bar
  • HX-Reswap - allows you to specify how the response will be swapped. See hx-swap for possible values
  • HX-Retarget - a CSS selector that updates the target of the content update to a different element on the page
  • HX-Reselect - a 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-Trigger - allows you to trigger client-side events
  • HX-Trigger-After-Settle - allows you to trigger client-side events after the settle step
  • HX-Trigger-After-Swap - allows you to trigger client-side events after the swap step

For more on the HX-Trigger headers, see HX-Trigger Response Headers.

Submitting a form via htmx has the benefit of no longer needing the Post/Redirect/Get Pattern. After successfully processing a POST request on the server, you don't need to return a HTTP 302 (Redirect). You can directly return the new HTML fragment.

Also, the response headers above are not provided to htmx for processing with 3xx Redirect response codes like HTTP 302 (Redirect). Instead, the browser will intercept the redirection internally and return the headers and response from the redirected URL. Where possible use alternative response codes like 200 to allow returning of these response headers.

Request Order of Operations

The order of operations in a htmx request are:

// TODO - redo

You can use the htmx-swapping and htmx-settling classes to create CSS transitions between pages.

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 to "true".

Animations

Htmx allows you to use CSS transitions in many situations using only HTML and CSS.

Please see the Animation Guide for more details on the options available.

Extensions

// TODO is this true?

In htmx 4, extensions are just libraries that hook into the standard events and use the public API. There is no longer a need for an explicit extension API.

Core Extensions

htmx supports a few core extensions, which are supported by the htmx development team:

  • head-support - support for merging head tag information (styles, etc.) in htmx requests
  • idiomorph - supports the morph swap strategy using idiomorph
  • response-targets - allows you to target elements based on HTTP response codes (e.g. 404)
  • ws - support for Web Sockets

You can see all available extensions on the Extensions page.

Creating Extensions

If you are interested in adding your own extension to htmx, please see the extension docs.

Events & Logging

Htmx has an extensive events mechanism, which doubles as the logging system.

If you want to register for a given htmx event you can use

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

or, if you would prefer, you can use the following htmx helper:

htmx.on("htmx:after:init", function(evt) {
    myJavascriptLib.init(evt.detail.elt);
});

The htmx:load event is fired every time an element is loaded into the DOM by htmx, and is effectively the equivalent to the normal load event.

Some common uses for htmx events are:

Initialize A 3rd Party Library With Events

Using the htmx:load event to initialize content is so common that htmx provides a helper function:

htmx.onLoad(function(target) {
    myJavascriptLib.init(target);
});

This does the same thing as the first example, but is a little cleaner.

Configure a Request With Events

You can handle the htmx:configRequest event in order to modify an AJAX request before it is issued:

document.body.addEventListener('htmx:configRequest', function(evt) {
    evt.detail.parameters['auth_token'] = getAuthToken(); // add a new parameter into the request
    evt.detail.headers['Authentication-Token'] = getAuthToken(); // add a new header into the request
});

Here we add a parameter and header to the request before it is sent.

Modifying Swapping Behavior With Events

You can handle the htmx:beforeSwap event in order to modify the swap behavior of htmx:

document.body.addEventListener('htmx:beforeSwap', function(evt) {
    if(evt.detail.xhr.status === 404){
        // alert the user when a 404 occurs (maybe use a nicer mechanism than alert())
        alert("Error: Could Not Find Resource");
    } else if(evt.detail.xhr.status === 422){
        // allow 422 responses to swap as we are using this as a signal that
        // a form was submitted with bad data and want to rerender with the
        // errors
        //
        // set isError to false to avoid error logging in console
        evt.detail.shouldSwap = true;
        evt.detail.isError = false;
    } else if(evt.detail.xhr.status === 418){
        // if the response code 418 (I'm a teapot) is returned, retarget the
        // content of the response to the element with the id `teapot`
        evt.detail.shouldSwap = true;
        evt.detail.target = htmx.find("#teapot");
    }
});

Here we handle a few 400-level error response codes that would normally not do a swap in htmx.

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 somt tips:

The first debugging tool you can use is the htmx.logAll() method. This will log every event that htmx triggers and will allow you to see exactly what the library is doing.

htmx.logAll();

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.

// TODO finalize methods 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.

Scripting

While htmx encourages a hypermedia approach to building web applications, it offers many options for client scripting. Scripting is included in the REST-ful description of web architecture, see: Code-On-Demand. As much as is feasible, we recommend a hypermedia-friendly approach to scripting in your web application:

The primary integration point between htmx and scripting solutions is the events that htmx sends and can respond to.

See the SortableJS example in the 3rd Party Javascript section for a good template for integrating a JavaScript library with htmx via events.

We have an entire chapter entitled "Client-Side Scripting" in our book that looks at how scripting can be integrated into your htmx-based application.

The hx-on* Attributes

HTML allows the embedding of inline scripts via the onevent properties, such as onClick:

<button onclick="alert('You clicked me!')">
    Click Me!
</button>

This feature allows scripting logic to be co-located with the HTML elements the logic applies to, giving good Locality of Behaviour (LoB).

Unfortunately, HTML only allows on* attributes for a fixed number of specific DOM events (e.g. onclick) and doesn't provide a generalized mechanism for responding to arbitrary events on elements.

In order to address this shortcoming, htmx offers hx-on:* attributes. These attributes allow you to respond to any event in a manner that preserves the LoB of the standard on* properties.

If we wanted to respond to the click event using an hx-on attribute, we would write this:

<button hx-on:click="alert('You clicked me!')">
    Click Me!
</button>

So, the string hx-on, followed by a colon (or a dash), then by the name of the event.

For a click event, of course, we would recommend sticking with the standard onclick attribute. However, consider an htmx-powered button that wishes to add a parameter to a request using the htmx:config:request event. This would not be possible using a standard on* property, but it can be done using the hx-on:htmx:config:request attribute:

// TODO verify symbols

<button hx-post="/example"
        hx-on:htmx:config-request="request.parameters.example = 'Hello Scripting!'">
    Post Me!
</button>

Here the example parameter is added to the POST request before it is issued, with the value 'Hello Scripting!'.

Another usecase is to reset user input on successful requests using the htmx:after:swap event, avoiding the need for something like an out-of-band swap.

3rd Party Javascript

Htmx integrates well with third party 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.

Web Components

Please see the Web Components Examples page for examples on how to integrate htmx with web components.

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. 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. Always disable htmx.config.historyRestoreAsHxRequest so that these history full HTML requests are not cached with partial fragment responses.

// TODO - verify if still needed

If you are unable (or unwilling) to use the Vary header, you can alternatively set the configuration parameter getCacheBusterParam to true. If this configuration variable is set, htmx will include a cache-busting parameter in GET requests that it makes, which will prevent browsers from caching htmx-based and non-htmx based responses in the same cache slot.

htmx also works with ETag as expected. Be mindful that if your server can render different content for the same URL (for example, depending on the value of the HX-Request header), the server needs to generate a different ETag for each content.

Security

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.

Configuration Options

htmx also provides configuration options related to security:

  • htmx.config.selfRequestsOnly - if set to true, only requests to the same domain as the current document will be allowed
  • htmx.config.allowScriptTags - htmx will process <script> tags found in new content it loads. If you wish to disable this behavior you can set this configuration variable to false
  • htmx.config.allowEval - can be set to false to disable all features of htmx that rely on eval:
    • event filters
    • hx-on: attributes
    • hx-vals with the js: prefix
    • hx-headers with the js: prefix

Note that all features removed by disabling eval() can be reimplemented using your own custom javascript and the htmx event model.

Events

TODO: rewrite

If you want to allow requests to some domains beyond the current host, but not leave things totally open, you can use the htmx:validateUrl event. This event will have the request URL available in the detail.url slot, as well as a sameHost property.

You can inspect these values and, if the request is not valid, invoke preventDefault() on the event to prevent the request from being issued.

document.body.addEventListener('htmx:validateUrl', function (evt) {
  // only allow requests to the current server as well as myserver.com
  if (!evt.detail.sameHost && evt.detail.url.hostname !== "myserver.com") {
    evt.preventDefault();
  }
});

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.

Here is an example CSP in a meta tag:

    <meta http-equiv="Content-Security-Policy" content="default-src 'self';">

This tells the browser "Only allow connections to the original (source) domain". This would be redundant with the htmx.config.selfRequestsOnly, but a layered approach to security is warranted and, in fact, ideal, when dealing with application security.

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.

CSRF Prevention

TODO: verify

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.

Note: hx-boost does not update the <html> or <body> tags; if using this feature with hx-boost, make sure to include the CSRF token on an element that will get replaced. Many web frameworks support automatically inserting the CSRF token as a hidden input in HTML forms. This is encouraged whenever possible.

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

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

Configuring htmx

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

They are listed below:

TODO: audit

Config Variable Info
htmx.config.defaultSwapStyle defaults to outerHTML
htmx.config.includeIndicatorStyles defaults to true (determines if the indicator styles are loaded)
htmx.config.indicatorClass defaults to htmx-indicator
htmx.config.requestClass defaults to htmx-request
htmx.config.addedClass defaults to htmx-added
htmx.config.settlingClass defaults to htmx-settling
htmx.config.swappingClass defaults to htmx-swapping
htmx.config.allowEval defaults to true, can be used to disable htmx's use of eval for certain features (e.g. trigger filters)
htmx.config.allowScriptTags defaults to true, determines if htmx will process script tags found in new content
htmx.config.inlineScriptNonce defaults to '', meaning that no nonce will be added to inline scripts
htmx.config.attributesToSettle defaults to ["class", "style", "width", "height"], the attributes to settle during the settling phase
htmx.config.inlineStyleNonce defaults to '', meaning that no nonce will be added to inline styles
htmx.config.useTemplateFragments defaults to false, HTML template tags for parsing content from the server (not IE11 compatible!)
htmx.config.wsReconnectDelay defaults to full-jitter
htmx.config.wsBinaryType defaults to blob, the type of binary data being received over the WebSocket connection
htmx.config.disableSelector defaults to [hx-disable], [data-hx-disable], htmx will not process elements with this attribute on it or a parent
htmx.config.withCredentials defaults to false, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates
htmx.config.timeout defaults to 0, the number of milliseconds a request can take before automatically being terminated
htmx.config.scrollBehavior defaults to 'instant', the scroll behavior when using the show modifier with hx-swap. The allowed values are instant (scrolling should happen instantly in a single jump), smooth (scrolling should animate smoothly) and auto (scroll behavior is determined by the computed value of scroll-behavior).
htmx.config.defaultFocusScroll if the focused element should be scrolled into view, defaults to false and can be overridden using the focus-scroll swap modifier.
htmx.config.getCacheBusterParam defaults to false, if set to true htmx will append the target element to the GET request in the format org.htmx.cache-buster=targetElementId
htmx.config.globalViewTransitions if set to true, htmx will use the View Transition API when swapping in new content.
htmx.config.methodsThatUseUrlParams defaults to ["get", "delete"], htmx will format requests with these methods by encoding their parameters in the URL, not the request body
htmx.config.selfRequestsOnly defaults to true, whether to only allow AJAX requests to the same domain as the current document
htmx.config.ignoreTitle defaults to false, if set to true htmx will not update the title of the document when a title tag is found in new content
htmx.config.scrollIntoViewOnBoost defaults to true, whether or not the target of a boosted element is scrolled into the viewport. If hx-target is omitted on a boosted element, the target defaults to body, causing the page to scroll to the top.
htmx.config.triggerSpecsCache defaults to null, the cache to store evaluated trigger specifications into, improving parsing performance at the cost of more memory usage. You may define a simple object to use a never-clearing cache, or implement your own system using a proxy object
htmx.config.responseHandling the default Response Handling behavior for response status codes can be configured here to either swap or error
htmx.config.allowNestedOobSwaps defaults to true, whether to process OOB swaps on elements that are nested within the main response element. See Nested OOB Swaps.
htmx.config.historyRestoreAsHxRequest defaults to true, Whether to treat history cache miss full page reload requests as a "HX-Request" by returning this response header. This should always be disabled when using HX-Request header to optionally return partial responses

You can set them directly in javascript, or you can use a meta tag:

<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>

Conclusion

And that's it!

Have fun with htmx!

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