This article was last updated on August 21, 2024, to add sections on State Management with HTMX and Error Handling with HTMX.
Introduction
In this article, we will explore what HTMX is and its capabilities.
HTMX is a small (14k min. gzipped), dependency-free javascript library that can create cutting-edge user interfaces with the ease and power of hypertext (markup). It provides access to AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML using attributes. This has become a game-changer for developers as it enables them to achieve interactivity (that JavaScript gives) from the markup alone.
Steps to follow in this article:
Installing HTMX
You can install HTMX using three methods:
Through A CDN: You can load HTMX to your website/application by including the CDN at the head of your HTML file.
<script
src="https://unpkg.com/htmx.org@1.9.6"
integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"
crossorigin="anonymous"
></script>
N/B: While this is possible,it is not advisable to use CDNs in production environments.
You can download a copy of the HTMX source file intoyour website/application
Download htmx.min.js
from unpkg.com, place it in the proper directory in your project, then include it with a script> tag as needed:
<script src="/path/to/htmx.min.js"></script>
Installing through npm: you can install HTMX through npm using the below command
npm install htmx.org
After installing, you'll need to use the proper tooling to utilize node_modules/htmx.org/dist/htmx.js
or node_modules/htmx.org/dist/htmx.min.js)
.
For instance, you may package HTMX with certain extensions and project-specific code.
Setting up HTMX in Webpack: If you are using webpack to manage your javascript, firstly, Install htmx via your package manager (npm, yarn, pnpm) and then add the import to your index.js
import "htmx.org";
If you want to use the global htmx variable, you can inject it into the window scope as follows:
- Create a unique JS file.
- Import this file into your
index.js
file.
import "path/to/my_custom.js";
- Then add the code below to the file
window.htmx = require("htmx.org");
- Finally, rebuild your application bundle(to reflect the changes)
Ajax Requests with HTMX
HTMX provides custom attributes that allow you to perform AJAX requests directly from the HTML:
Attribute | Description |
---|---|
hx-post | fires a post request to a given url |
hx-get | fires a get request request to a given url |
hx-put | fires a put request to a given url |
hx-patch | fires a patch request to a given url |
hx-delete | fires a delete request to a given url |
Use case
<div hx-post="/messages">Put To Messages</div>
from the code above the hx-post
attribute triggers a post api call to the url "/messages"
Trigger Request
In HTMX, an element's natural event initiates AJAX requests. For instance, the onchange event triggers the input
, select
, and textarea
; the onsubmit event triggers the form
; and the onclick event triggers everything else.
However, HTMX offers a unique hx-trigger attribute for situations when you want to change the event that initiates the request:
<div hx-post="http://localhost:3000/submit" hx-trigger="mouseenter">
Submit
</div>
In the code above, only if the user's mouse hovers above the div will the GET request be made to the specified URL.
Trigger Modifiers
A trigger's behavior can also be altered by a few other modifications. Use the once modifier for the trigger, for instance, if you want a request to occur only once:
<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
Submit
</div>
Other trigger modifiers you can use are: changed - Only send a request if the element's value has changed
delay:<time interval>
- It specifies a delay period before making the request. The countdown is reset if the event occurs again.
throttle:<interval time>
- it specifies a delay time before making the request,. In contrast to delay, if a new event happens before the time limit is reached, the event is deleted, and the request is triggered at the end of the time period.
from:<CSS Selector>
- listen to the event of a different element.
to view more modifiers that HTMX supports, visit here
Trigger Filters
These are applied by surrounding a javascript expression in square brackets after the event name. If the expression evaluates to true, the event will be triggered; otherwise, it will not.
<div hx-get="/clicked" hx-trigger="click[ctrlKey]">
Click the force
</div>
htmx includes a few special events that can be used with hx-trigger:
- load - fires once when the element is loaded for the first time.
- revealed - fires when an element first scrolls into the viewport
- intersect - fires when an element first intersects the viewport. This allows for two additional options:
- root:
<selector>
- a root element CSS selector for intersection - threshold:
<float>
- a floating point number between 0.0 and 1.0 indicating the amount of intersection on which the event should be triggered.
- root:
If you have a more advanced use case, you may also utilize custom events to trigger requests.
Polling
you can use the every
syntax with a timeframe (in seconds) with the hx-trigger property to make an element poll a specific URL:
<div hx-get="/get-star-wars-data" hx-trigger="every 2s"></div>
Load Polling
"Load polling" is an additional method for doing polling in HTMLX. It involves an element specifying a load trigger and a delay, and then replacing itself with the response:
<div
hx-get="/get-star-wars-data"
hx-trigger="load delay:1s"
hx-swap="starWarsDataHTML"
></div>
Request Indicators
Because the browser does not provide feedback when an AJAX request is made, it is often beneficial to notify the user that anything is happening. You may do this with HTMX by using the htmx-indicator class.
The opacity of any element with the htmx-indicator class is set to 0 by default, meaning that even though the element is present in the DOM, it is invisible.
A htmx-request class is added to an element when HTMX submits a request. When a child element with the htmx-indicator class on it is exposed to the htmx-request class, it will transition to an opacity of 1, displaying the indicator.
<button hx-get="/click">
Click Me!
<img class="htmx-indicator" src="/spinner.gif">
</button>
Using the hx-indicator attribute and a CSS selector, you can add the htmx-request class to an alternative element if you wish:
<div>
<button hx-get="/click" hx-indicator="#indicator">
Click the force!
</button>
<img id="indicator" class="htmx-indicator" src="/spinner.gif" />
</div>
Targets
You can load the response into an element other than the one that initiated the request using the hx-target attribute which accepts a CSS selector(a div, class, etc).
<input type="text" name="star-wars-input"
hx-get="/star-wars-characters"
hx-trigger="keyup delay:500ms changed"
hx-target="#search-results"
placeholder="Search..."
>
<div id="search-results"></div>
In the example above, the hx-target attribute targets the #search-result
id and populate request values to the div containing the id when the request is successful.
Swapping
You can swap HTML contents returned to the DOM in HTMX. The target element's innerHTML
is automatically replaced with the content. This can be changed by applying any of the following values to the hx-swap attribute:
Name | Description |
---|---|
innerHTML | the content is inserted inside the target element by default. |
outerHTML | the returning content completely replaces the target element. |
afterbegin | content is prepended before the target's first child |
beforebegin | prepends the content of the target's parent element before the target. |
beforeend | adds the content after the target's final child. |
afterend | adds the content to the targets parent element after the target |
delete | removes the target element irrespective of the response |
none | does not include the response's content |
an example of hx-swap in action
<div hx-get="/example" hx-swap="afterend">
Get Some HTML & Append It
</div>
In the code above, the div
will make a request and append the returned contents behind the div
.
Synchronization
When you make requests in two elements and you want one of them to take priority over the other or wait until the other element's request is fulfilled, HTMX has a hx-sync feature that can be helpful.
For example, let us paint a scenario of a race situation between a form submission and a validation request for a specific input:
<form hx-post="/submit-character">
<input id="title" name="title" type="text"
hx-post="/validate-character"
hx-trigger="change"
>
<button type="submit">Submit</button>
</form>
When the input is completed and the form is instantly submitted, two concurrent requests are sent to /validate-character
and /submit-character
without the use of hx-sync.
When hx-sync="closest form:abort" is applied to an input, the form will be watched for requests and the input request will be aborted if a form request is found or begins while the input request is being processed. This automatically solves the synchronization constraint.
<form hx-post="/submit-character">
<input id="title" name="title" type="text"
hx-post="/validate-character"
hx-trigger="change"
hx-sync="closest form:abort"
>
<button type="submit">Submit</button>
</form>
Additionally, htmx offers a programmatic method for cancelling requests: To stop any in-flight requests, you can send an element the htmx:abort event into a htmx.trigger function:
<button id="request-button" hx-post="/submit-character">
Submit Character
</button>
<button onclick="htmx.trigger('#request-button', 'htmx:abort')">
Cancel Submission
</button>
CSS Transitions
HTMX lets you perform CSS Transitions without the need for javascript. we will illustrate this with an example:
<div id="div1">The Empire</div>
We then make an ajax request and replace the content with the content below:
<div id="div1" class="red">
The Republic
</div>
From the code above, a new class red
has been added to the new content. We can create a CSS transition from the previous state(without the class red
) to the current one (with the class red
) in this scenario:
.red {
color: red;
transition: all ease-in 1s;
}
How HTMX handles transitions under the hood:
Before new content is added to a page, elements that match the id
attribute of the current content are found. This happens when new content is received from a server. Before the swap takes place, the properties of the old content are replicated onto the new element if a match is found for it in the updated content. After that, the new content is replaced, but the old attribute values remain. The new attribute values are then switched in following a "settle" time (20 ms by default).
Out of Band Swaps
The hx-swap-oob attribute in the response html can be used to swap content from a response directly into the DOM using the id
attribute:
<div id="message" hx-swap-oob="true">Dark Sidious</div>
Anakin Skywalker
from the code above, The extra content would be swapped into the target in the typical way, but div#message
would be swapped straight into the corresponding DOM element in this response.
Parameters
You can filter the arguments that will be submitted with an AJAX call using the hx-params attribute.
These are the possible values for this attribute:
"*"- Include every parameter by default
none - Do not include
<param-list>
and do not include any parameters. All except the parameter names list<param-list>
, which should be comma-separated Provide a list of all the parameter names, separated by commas.
<div hx-get="/get-starwars-characters" hx-params="*">
Luke Skywalker
</div>
In the example above, All the parameters that a POST would contain are contained in this div, however as is customary with a GET, they will be URL encoded and included in the URL.
Confirming Requests
HTMX provides the hx-confirm attribute, which enables you to confirm an action using a straightforward javascript dialogue:
<button
hx-delete="/delete-character"
hx-confirm="Are you sure you wish to delete Obiwan Kenobi"
>
Delete Obiwan Kenobi
</button>
Web Sockets & SSE
HTMX currently has experimental supports for WebSockets and Server Sent Events. However, if you want to establish a WebSocket connection, you can use the hx-ws attribute
<div hx-ws="connect:wss:/darth-chatroom">
<div id="chat_room">
...
</div>
<form hx-ws="send:submit">
<input name="chat_message">
</form>
</div>
To send events to browsers using SSE(Server sent Events), add a connect <url>
declaration to a parent element and add the hx-sse attribute with the URL.
After which, you will use the hx-trigger="sse:<event_name>
attribute to define elements that are this element's descendants and are activated by server-sent events. grammar
A sample is shown below
<body hx-sse="connect:/darth-events">
<div hx-trigger="sse:new_news" hx-get="/news"></div>
</body>
History Support
With the hx-push-url attribute on an element you can push a request URL into the browser's navigation bar and add the page's current state to the history:
<a hx-get="/star-wars-characters" hx-push-url="true">
Blog
</a>
Principle on how the hx-push-url works:
Before making a request to /star-wars-characters
, HTMX will take a snapshot of the current document and store it when a user clicks on this link. After that, a new location is added to the history stack and the swap is completed.
HTMX simulates "going back" to the prior state when a user presses the back button by retrieving the old content from storage and swapping it back into the target. HTMX will send an ajax call to the specified URL with the header HX-History-Restore-call
set to true
if the location cannot be located in the cache, and it will expect back the HTML required for the full page. Alternatively, it will force a hard browser refresh if the htmx.config.refreshOnHistoryMiss
config setting is set to true.
Validation with HTMX
Because HTMX works with the HTML5 Validation API, an invalid validatable input will prevent a form request from being made. This applies to both WebSocket sends and AJAX queries.
Events related to validation in HTMX are:
htmx:validation:validate - called before an elements checkValidity() method is called. May be used to add in custom validation logic
htmx:validation:failed - called when checkValidity() returns false, indicating an invalid input
htmx:validation:halted - called when a request is not issued due to validation errors. Specific errors may be found in the event.detail.errors object
<form hx-post="/create-characters">
<input _="on htmx:validation:validate
if my.value != 'foo'
call me.setCustomValidity('Please enter the value foo')
else
call me.setCustomValidity('')"
name="example"
>
</form>
The code above is an illustration of an input using the htmx:validation. In the code, hyperscript was used to validate the event to ensure that an input have the value foo
.
State Management Using HTMX
I'll just share some thoughts on how the whole state management thing works in HTMX, and how this might compare with how you've been doing it with traditional JavaScript frameworks like React and Vue.
1. Server-Driven State
HTMX is state management with a server-driven approach. It means most of the logic and state handling are done on the server rather than on the client. This contrasts sharply with JavaScript frameworks like React that handle state on the client side. Each and every action or interaction in HTMX (like form submissions and button clicks) creates a request that's made to a server. In response, it receives HTML back from the server to update the client-side DOM without any further effort on your part.
For example:
<div hx-get="/get-data" hx-trigger="click" hx-swap="outerHTML">
Click to Get Data
</div>
Here, the state is maintained on the server, and HTMX will update the DOM according to the response from the server to keep the client in sync with the latest state.
2. Client-Side State Management Not Possible
Unlike React, where state is managed using useState
and useReducer
, HTMX relies on very little client-side state. Then, if you are in a server-centric world—which is quite good if your application is not one of these super-client interactive applications—the state management is easier, and you do not have to have all these huge, heavy client-side tools managing the state for you.
3. Forms and Inputs Handling
For forms, HTMX supports even form submission in an asynchronous fashion, whereby even the state of the form can be handled by directly listening on the server's responses. This eliminates all the boilerplate of tracking client-side state: the server is responsible for validation, processing, and changing state and then updating the UI accordingly.
Sample:
<form hx-post="/submit-form" hx-swap="outerHTML">
<input name="username" type="text" />
<button type="submit">Submit</button>
</form>
In this case, the server is in control of the form submission, and it directly sends the server response back to the DOM, updating it accordingly.
4. Persistence Across Requests
Because HTMX is essentially server-driven, it can be used most productively with server-side session state or database-driven state that can be preserved across requests. This makes handling things like persistent user sessions, authentication, and long-running operations easy without the need to store state in the browser or client.
5. Dealing with Complex State
If your app needs more complex state management—like multiple interacting components or maintaining state across different elements...
Animations with HTMX
HTMX is intended to allow you to add seamless animations and transitions to your web page using only CSS and HTML. You can now utilize the new View Transitions API to create animations with HTMX. A basic example is the Fade Out On Swap animation. If you want to fade out an element that will be removed when the request is finished, use the htmx-swapping class with some CSS and extend the swap phase to be long enough for your animation to finish. This can be accomplished as follows:
<style>
.fade-me-out.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;
}
</style>
<button class="fade-me-out"
hx-delete="/fade_out_demo"
hx-swap="outerHTML swap:1s">
Fade Me Out
</button>
more examples of animations with HTMX are shown here
Extensions in HTMX
HTMX includes an extension framework for creating and deploying extensions within htmx-based applications. With the hx-ext attribute You can utilize extension libraries(after being specified in javascript).
Using an extension entails two steps:
- Include the extension definition, which will add it to the HTMX extension registry.
- The hx-ext attribute is used to specify the extension.
An example is shown below:
// Including the extension library
<script src="/path/to/ext/debug.js" defer></script>
// add the extension(debug) to the hx-ext attribute
<button hx-post="/example" hx-ext="debug">
This Button Uses The Force
</button>
HTMX comes with a set of extensions that cover common developer needs. In each release, these extensions are tested against htmx. In order to view the list of included extensions in HTMX, visit here
Logging and events in HTMX
Events
HTMX includes a robust events mechanism that also serves as the logging system.
You can easily register to a HTMX event through: The event listener approach:
document.body.addEventListener("htmx:load", function (evt) {
myJavascriptLib.init(evt.detail.elt);
});
HTMX helpers:
htmx.on("htmx:load", function (evt) {
myJavascriptLib.init(evt.detail.elt);
});
in both cases, htmx:load
is the event.
The htmx:load
event is fired whenever HTMX loads an element into the DOM, and it is functionally the same as the conventional load event.
You can also initialize a third-party library Using the htmx:load
event:
htmx.onLoad(function (target) {
myJavascriptLib.init(target);
});
You can also configure a request with events. An example of this is shown below:
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
});
In the code above, we used the htmx:configRequest
event to change an AJAX request before it is sent:
You can adjust the swap behavior of HTMX by handling the htmx:beforeSwap
event.
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");
}
});
In the code above, we handled a few 400
-level error response codes that would ordinarily not result in a swap in htmx.
In order to view more events that HTMX supports, visit here.
Logging
HTMX provides a htmx.logger
variable which when set, logs every event. An example on how to set the logger is shown below:
htmx.logger = function (elt, event, data) {
if (console) {
console.log(event, elt, data);
}
};
Error Handling in HTMX
I have a few thoughts about how error handling works in HTMX and how we could manage errors gracefully in our applications.
1. Simple Error Handling
HTMX has built-in events that can help us catch Ajax request errors or form submission errors in a custom manner. With an error like 404, 500, or a validation failure, we get an opportunity to intercept the error using the event htmx:responseError
.
Here's how you would handle a trivial error event:
document.body.addEventListener("htmx:responseError", function (evt) {
alert("An error occurred: " + evt.detail.xhr.status);
});
This event is triggered every time an AJAX request fails, and you can specify the response based on the HTTP status code, such as 404 or 500.
2. Presenting Error Messages to Users
Provide friendly UI error messages on page failure, rather than a pure alert or default browser error, by listening for the event htmx:beforeSwap
and injecting error content into an area of the page you designate:
document.body.addEventListener("htmx:beforeSwap", function (evt) {
if (evt.detail.xhr.status >= 400) {
evt.detail.shouldSwap = false;
document.getElementById("error-message").innerHTML =
"Something went wrong. Please try again.";
}
});
In this case, an error will prevent a default content swap and instead show the content of a custom error message in a #error-message
div.
3. Handling Form Validation Errors
With HTMX, form validation is very easy to handle. For example, if the server returns an error about validation (e.g., 422 Unprocessable Entity), we can easily manage such a response and show validation messages without page reloading.
Here's an example:
<form hx-post="/submit" hx-target="#form-errors" hx-swap="outerHTML">
<input name="username" type="text" />
<button type="submit">Submit</button>
</form>
<div id="form-errors"></div>
If the server returns validation errors, those can be added to the response and will be shown inside the #form-errors
without a page reload.
4. Graceful Fallback for Non-HTMX Browsers
One thing HTMX is really good at is being able to progressively enhance your application, but you need to handle errors gracefully for your non-HTMX users—that is, those with JavaScript disabled. In that case, we return a full-page reload in case of necessity, so the classic form submission and error handling still work.
5. Request-Specific Error Handling
HTMX allows you to work with errors on a per-request basis by attaching the error handling logic to specific elements. For instance, if there is a button that triggers an AJAX request, you could listen for events like:
document
.getElementById("my-button")
.addEventListener("htmx:responseError", function (evt) {
console.log("Error on this button request: " + evt.detail.xhr.status);
});
This allows granular error handling, where it is easy to distinguish the error responses for different parts of an application.
6. Handling Different HTTP Error Codes
HTMX does have some flexibility in how you can treat various HTTP error codes. For example, you might want to treat a 404
error differently from a 500
error or handle a 403
Forbidden by redirecting users to a login page:
document.body.addEventListener("htmx:beforeSwap", function (evt) {
if (evt.detail.xhr.status === 404) {
alert("Page not found.");
evt.detail.shouldSwap = false;
} else if (evt.detail.xhr.status === 403) {
window.location.href = "/login";
}
});
For instance, we show an alert for 404
errors, and in case a 403
Forbidden error occurs, the user is redirected to the login page.
7. Programmatically Aborting Requests
Sometimes, you would want to programmatically cancel ongoing requests when a new one is triggered. HTMX provides the htmx:abort
event for this:
<button id="submit-btn" hx-post="/submit" hx-target="#result">Submit</button>
<button onclick="htmx.trigger('#submit-btn', 'htmx:abort')">
Cancel Request
</button>
This will cancel all requests made by the Submit
button.
Conclusion
In this article, we provided an overview of the HTMX library. we showed that you can achieve Ajax functionality without relying on JavaScript with HTMX, allowing you to construct dynamic applications at ease. HTMX may be more than just a new library. It may revolutionize the way we approach interactivity in web development.