Editor’s note: This article was updated on 18 May 2024 to reflect the most recent version of htmx, expand on its practical use cases, discuss the community, ecosystem, and ways to extend htmx, address some criticisms of htmx and its limitations, compare it with similar technologies, and more.
Bare simplicity isn’t very common in modern web development. Libraries like Astro, Vue, and React, along with innovative solutions like server components, are transforming how we build web applications. However, their DX often doesn’t match the straightforwardness of classic libraries like jQuery. The htmx library aims to bridge this gap.
In this tutorial, we’ll explore htmx, go over how it works, learn how to get started using it with code samples, and also compare it with other similar libraries.
htmx is a JavaScript library for performing AJAX requests, triggering CSS transitions, and invoking WebSocket and server-sent events directly from HTML elements via custom attributes. It lets you build interactive user interfaces with simple markups.
The htmx library weighs ~14KB (min.gz’d), is dependency-free (i.e., does not require any other JavaScript packages to run), and is also compatible with Internet Explorer v11.
Making asynchronous requests and updating interfaces is a fundamental task in frontend development. Traditionally, this involves using JavaScript to handle events and update the UI.
However, with htmx, things are much simpler. You can achieve the same functionality directly in your markup, as shown below:
<button hx-get="/path/to/api" hx-swap="outerHTML">Click Me</button>
In this example, once you click the button, your web application sends a GET
request to path/to/api
and will replace the content of the button with the response returned from the API.
Simplicity is one of the major benefits of htmx. It positions the library to help you get things done quickly while writing minimal code.
Furthermore, as mentioned at the beginning of this article, htmx is super lightweight, weighing ~14 KB (min.gz’d). For context, this is similar in size to Axio’s latest version (1.7.2), which weighs around 13.2 KB.
Understandably, Axios offers advanced HTTP request features; however, htmx goes beyond just making HTTP requests, which makes its size an impressive feat.
Of course, htmx is not without its drawbacks. For example, its AJAX feature set doesn’t work well with REST and GraphQL APIs.
Additionally, htmx is designed for hypermedia APIs, which return custom markup rather than structured JSON data. For this reason, working with REST and GraphQL APIs can be a bit tricky.
You can get started with htmx by including its CDN link directly in your markup, like below:
<script src="https://unpkg.com/[email protected]"></script>
The script above loads the current stable version of htmx — at the time of writing, version 1.9.12 — on your webpage. Alternatively, you can install htmx with npm using the command below:
npm install htmx.org
Once that’s done, you can now start implementing htmx features on your webpage.
htmx provides a set of attributes that allows you to send AJAX requests directly from an HTML element. Available attributes include:
hx-get
: Send GET
request to the provided URLhx-post
: Send POST
request to the provided URLhx-put
: Send PUT
request to the provided URLhx-patch
: Send PATCH
request to the provided URLhx-delete
: Send DELETE
request to the provided URLThe code example below tells the browser that when the user clicks the button, it sends a GET
request (hx-get
) to the provided URL, which in this case is http://localhost/todos
:
<button hx-get="http://localhost/todos">Load Todos</button>
The result should look something like this:
By default, the response returned from any htmx request will be loaded in the current element that is sending the request. In the “Targeting elements for AJAX requests” section below, we will further explore how to load the response in another HTML element.
AJAX requests in htmx are triggered by the natural event of the element. For example, input
, select
, and textarea
are triggered by the onchange
event, and form
is triggered by the onsubmit
event, and every other thing is triggered by the onclick
event.
In a situation where you want to modify the event that triggers the request, htmx provides a special hx-trigger
attribute for this:
<div hx-get="http://localhost/todos" hx-trigger="mouseenter"> Mouse over me! </div>
In this example, the GET
request will be sent to the provided URL if and only if the user’s mouse hovers on the div.
The hx-trigger
attribute mentioned in the previous section accepts an additional modifier to change the behavior of the trigger. Available trigger modifiers include:
once
: Ensures a request will only happen oncechanged
: Issues a request if the value of the HTML element has changeddelay:<time interval>
: Waits for the given amount of time before issuing the request (e.g., delay-1s
). If the event triggers again, the countdown is resetthrottle:<time interval>
: Waits the given amount of time before sending the request (e.g., throttle:1s
). But unlike delay, if a new event occurs before the time limit is reached, the event will be in a queue so that it will trigger at the end of the previous eventfrom:<CSS Selector>
: Listens for the event on a different elementLet’s see a trigger modifier in action:
<input type="text" hx-get="http://localhost/search" hx-trigger="keyup changed delay:500ms" />
In the code sample provided above, once the user performs a keyup
event on the input element — i.e., the user types any text in the input box — and its previous value changes, the browser will automatically send a GET
request to http://localhost/search
after 500ms.
htmx-trigger
attributeIn the htmx-trigger
attribute, you can also specify every n
seconds rather than waiting for an event that triggers the request. With this option, you can send a request to a particular URL every n
seconds:
<div hx-get="/history" hx-trigger="every 2s"> </div>
The code sample above tells the browser to issue a GET request to /history
endpoint every two seconds and load the response into the div.
In previous sections, we mentioned that the response from an AJAX request in htmx will be loaded into the element making the request. If you need the response to be loaded into a different element, you can use the hx-target
attribute to do this.
The hx-target
attribute accepts a CSS selector and automatically injects the AJAX response into an HTML element with the specified selector. We can modify our to-do sample to suit this case:
<button hx-get="http://localhost/todos" hx-target="#result"> Load Todos </button> <div id="result"></div>
Unlike the previous example, this new code sample sends a request to http://localhost/todos
and loads the response in our div with id=result
.
Similar to hx-target
, the hx-swap
attribute is used to define how the returned AJAX response will be loaded in the DOM. Supported values include:
innerHTML
: Default value, this option will load the AJAX response inside the current element sending the requestouterHTML
: This option replaces the entire element sending the request with the returned responseafterbegin
: Loads the response as a first child of the element sending the requestbeforebegin
: Loads the response as a parent element of the actual element triggering the requestbeforeend
: Loads and appends the AJAX response after the last child of the element sending the requestafterend
: Unlike the previous, this appends the AJAX response after the element sending the requestnone
: This option will not append or prepend the response from an AJAX requestWhen sending an AJAX request, it’s often good practice to let the user know that something is happening in the background since the browser won’t do this automatically by default. You can easily accomplish this in htmx with the htmx-indicator
class.
Consider the code sample below:
<div hx-get="http://path/to/api"> <button>Click Me!</button> <img class="htmx-indicator" src="path/to/spinner.gif" /> </div>
The opacity of any HTML element defined with the htmx-indicator
class is set to 0
by default, making the element invisible but present in the DOM.
When you issue an AJAX request, htmx will automatically add a new htmx-request
class to the element sending the request. This new htmx-request
class will cause a child element with the htmx-indicator
class on it to transition to an opacity of 1
, therefore showing the indicator:
If your AJAX request was triggered by a form or an input element, then by default, htmx will automatically include the value of all the input field or fields in your request.
But in a case where you want 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:
<div> <button hx-post="/api/register" hx-include="[name=username]"> Register! </button> Enter Username: <input name="username" type="text"/> </div>
In the code sample above, when you issue a request to the /api/register
endpoint, your AJAX request will automatically include the username
field in its body.
htmx also provides another htmx-params
attribute, with which you can filter out the only parameters that will be submitted when an AJAX request is sent:
<div hx-get="http://path/to/api/example" hx-params="*"> Send Request </div>
The code sample above will include all input elements on your page as your request parameters.
All possible values include:
*
: Will include all parameters present in your webpage and send it along in your AJAX requestnone
: Won’t include any parameters in your requestnot <param-list>
: Includes all other parameters and excludes the comma-separated list of parameter names<param-list>
: Will only include all the comma-separated parameter names in your listWith htmx, you can easily send files such as images, videos, PDFs, and more to your backend for processing by adding the hx-encoding
attribute with the value multipart/form-data
to the parent element of the actual element sending the request:
<form hx-encoding="multipart/form-data"> Select File: <input type="file" name="myFile" /> <button hx-post="http://path/to/api/register" hx-include="[name='myFile']" > Upload File! </button> </form>
htmx is integrated with the HTML5 validation API by default, and will not issue a request if a validatable input is invalid. This feature works for both AJAX requests and WebSocket events.
In addition to this, htmx also fires events around validation, which can be pretty useful in custom input validation and error handling.
Available validation events include:
htmx:validation:validate
: This event is useful in adding custom validation login, as it is called before an element is validatedhtmx:validation:failed
: This event is fired when an element validation returns false, i.e., indicating an invalid inputhtmx:validation:halted
: This event is called when an element was unable to issue a request due to input validation errorshtmx can be further enhanced with its extensions, which allow you to add custom behaviors and integrations to your application.
To add an extension, first load the extension source file, then reference it with the hx-ext
attribute. For example, to process and render JSON data, we can use the client-side-templates extension and the mustache template engine, as demonstrated in the code sample below:
<body> <div hx-ext="client-side-templates"> <button hx-get="https://jsonplaceholder.typicode.com/users" hx-swap="innerHTML" hx-target="#content" mustache-array-template="foo" > Click Me </button> <p id="content">Todos will be loaded here</p> <template id="foo"> {{#data}} <p>{{name}} at {{email}} is with {{company.name}}</p> {{/data}} </template> </div> <script src="https://unpkg.com/htmx.org"></script> <script src="https://unpkg.com/[email protected]/dist/ext/client-side-templates.js"></script> <script src="https://unpkg.com/mustache@latest"></script> </body>
You can also attach multiple extensions to a single element by separating the extension names with a comma:
<button hx-post="/path/to/api" hx-ext="debug, class-tools"> This button uses multiple extensions </button>
Adding the htx-ext
attribute to an element causes the extension to affect all child elements of that parent node. However, you can disable extensions for such child elements by adding the ignore:<extensionName>
, as demonstrated in the example below:
<div hx-ext="debug"> <button hx-post="/path/to/api">This button uses the debug extension</button> <button hx-post="/path/to/api" hx-ext="ignore:debug">This button doesn't</button> </div> <script src="https://unpkg.com/htmx.org"></script> <script src="https://unpkg.com/[email protected]/dist/ext/debug.js"></script>
class-tool
extensionhtmx provides a way to easily attach smooth CSS transitions to AJAX events as well as in your webpage generally using the class-tool
extension. This extension allows you to toggle, add, or remove a particular class name from an HTML element without writing any JavaScript code.
You can utilize this extension by adding the classes
attribute to your element and then specifying the action, followed by the class name you want to add or remove:
<div classes="add sample-class:1s"></div> <script src="https://unpkg.com/[email protected]/dist/ext/class-tools.js"></script>
In the code sample above, once the browser content is loaded, htmx will automatically add a new class (sample-class
) to the div after 1
second.
Also note that you can create an action queue by separating each action with a comma (,
), or make multiple actions run simultaneously by separating them with an ampersand (&
):
<!-- class tool queue --> <div classes="add sample-class:1s, remove another-class:2s, toggle 3rd-class:1s"></div> <!-- simultaneous actions --> <div classes="add sample-class:1s & remove another-class:2s & toggle 3rd-class:1s"></div>
Below is an example that toggles the visibility of an element:
<style> .demo.faded { opacity: 0.3; } .demo { opacity: 1; transition: opacity ease-in 900ms; } </style> <div class="demo" classes="toggle faded:1s">I'm Fading! ⚡</div>
And here’s the output:
The latest version of htmx also includes extensions that allow you to interact with WebSocket servers. You can add this extension by including its CDN script into your htmx application, as demonstrated below:
<!-- Websocket extension --> <script src="https://unpkg.com/[email protected]/dist/ext/ws.js"></script>
With the extension loaded in your app, you can now connect to a web socket server using the ws-connect
attribute and send messages with ws-send
:
<div hx-ext="ws" ws-connect="/chatroom"> <div id="notifications"></div> <div id="chat_room">...</div> <form id="form"ws-send> <input name="chat_message" /> </form> </div>
As shown above, we are attempting to connect to a WebSocket server/chatroom. The responses — notifications
and chat_room
— returned by this server are automatically loaded into our application. In addition, we created a form that allows us to send messages back to this server using the ws-send
property.
Many people are skeptical about whether htmx is just a meme or an actual serious production-ready library, and it's easy to see why.
For one, the official X (formerly Twitter) account for htmx would often post humorous or intentionally low-quality content about the library itself. You can also literally apply to become the CEO of htmx by tweeting. Additionally, the library creator would often cross-share posts he didn't agree with on the official website.
However, all of this might be a marketing effort to get people talking about the library, which has worked pretty well so far. This article by Carson Gross is also worth reading for a more critical take on the library.
Whether or not htmx is a meme, it’s efficient for its intended purpose. It allows you to keep most of your logic within the HTML layer.
Furthermore, htmx is great for adding dynamic behavior to web pages without the complexities of a full JavaScript framework. This is especially handy for backend developers with limited Javascript experience who want to quickly start prototyping async requests on the front end.
It's worth mentioning that htmx is not the first library designed to simplify dynamic content updates. In the past, we've had libraries like Intercooler.js, PJax, and Unpoly. In comparison:
The table below explores the major differences between these libraries:
Feature | htmx | Intercooler.js | PJax | Unpoly |
---|---|---|---|---|
Primary Focus | Enhancing HTML with AJAX capabilities | Enhancing HTML with AJAX capabilities | Fast page navigation with AJAX | Fast, user-friendly interfaces |
Approach | Modern, flexible, minimal code | Less modern, similar concept | Replaces content, updates URL | Built-in utilities for common tasks |
Use Cases | Various asynchronous interactions, | Similar to htmx | Fast navigation, partial updates | User-friendly, flexible interactions |
Maintenance | Actively maintained, modern standards | Less support for modern standards | Primarily for navigation | Focus on user-friendly interactions |
Complexity | Lightweight, minimal JavaScript | Lightweight, minimal JavaScript | Moderate, focus on navigation | Moderate, more built-in utilities |
It's also important to understand that comparing htmx to libraries like React, Vue, and Svelte is not ideal. These are full-fledged frameworks and libraries for building complex, stateful applications with comprehensive solutions for state management and reusable components.
In contrast, htmx only enhances HTML with minimal JavaScript. This makes it useful for almost completely different project types and needs.
In this article, we’ve covered the htmx installation process, sending AJAX requests, connecting to a WebSocket server, form validation, and triggering CSS animations directly from our HTML code without writing any JavaScript.
The htmx community is steadily growing, with the library garnering over 33k stars on GitHub. Given its popularity and active development, we can expect even more features and improvements in the future.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowEfficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
Design React Native UIs that look great on any device by using adaptive layouts, responsive scaling, and platform-specific tools.
Angular’s two-way data binding has evolved with signals, offering improved performance, simpler syntax, and better type inference.
Fix sticky positioning issues in CSS, from missing offsets to overflow conflicts in flex, grid, and container height constraints.