An Introduction to htmx, the HTML-focused Dynamic UI Library

    James Hibbard
    Share

    Web users today expect the fluid, dynamic experiences that single-page applications (SPAs) deliver. However, creating SPAs often involves intricate frameworks like React and Angular, which can be complex to learn and work with. Enter htmx — a library that brings a fresh perspective to building dynamic web experiences by leveraging features such as Ajax and CSS transitions directly in HTML.

    In this guide, we’ll explore the capabilities of htmx, how it simplifies dynamic web development, and how you can harness its potential to enhance your web development process.

    Table of Contents
    1. What Is htmx and How Does It Work?
    2. Installing htmx
    3. Ajax Requests: the htmx Approach
    4. Triggering Requests with htmx
    5. Request Indicators
    6. Targeting Elements & Swapping Content
    7. CSS Transitions with htmx
    8. Form Validation
    9. What Else Can htmx Do?
    10. Conclusion

    What Is htmx and How Does It Work?

    When building interactive web experiences, developers have traditionally had two main options, each with its own trade-offs. On one hand, there are multi-page applications (MPAs) which refresh the entire page every time a user interacts with it. This approach ensures that the server controls the application state and the client faithfully represents it. However, the full page reloads can lead to a slow and clunky user experience.

    On the other hand, there are single-page applications (SPAs) which rely on JavaScript running in the browser to manage the application state. They communicate with the server using API calls, which return data, often in JSON format. The SPA then uses this data to update the user interface without a page refresh, providing a much smoother user experience somewhat akin to a native desktop or mobile app. However, this approach isn’t perfect either. The computational overhead is usually higher due to substantial client-side processing, the initial load times can be slower as the client has to download and parse large JavaScript bundles before rendering the first page, and setting up the development environment often involves dealing with intricate build tools and workflows.

    htmx provides a middle ground between these two extremes. It offers the user experience benefits of SPAs — with no need for full page reloads — while maintaining the server-side simplicity of MPAs. In this model, instead of returning data that the client needs to interpret and render, the server responds with HTML fragments. htmx then simply swaps in these fragments to update the user interface.

    This approach simplifies the development process by minimizing client-side complexity, as well as the substantial JavaScript reliance common to SPAs. It requires no elaborate setup and provides a smooth and responsive user experience.

    Installing htmx

    There are several ways to include htmx in your project. You could download it directly from the project’s GitHub page, or if you’re working with Node.js, you can install it via npm using the command npm install htmx.org.

    However, the simplest way, and the one we’ll be using in this guide, is to include it via a content delivery network (CDN). This allows us to start using htmx without any setup or installation process. Just include the following script tag in your HTML file:

    <script src="https://unpkg.com/htmx.org@1.9.4"></script> 
    

    This script tag points to version 1.9.4, but you can replace “1.9.4” with the latest version if a newer one is available.

    htmx is very lightweight, with a minified and gzipped version weighing at ~14KB. It has no dependencies and is compatible with all major browsers, including IE11.

    Once you’ve added htmx to your project, you might want to check that it’s working correctly. You can test this with the following simple example:

    <button
      hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode&type=single"
      hx-target="#joke-container"
    >
      Make me laugh!
    </button>
    
    <p id="joke-container">Click the button to load a joke...</p>
    

    When you click the button, if htmx is working correctly, it will send a GET request to the Joke API and replace the contents of the <p> tag with the server’s response.

    A SQL statement walks into a bar and sees two tables. It approaches and asks 'may I join you?'

    Ajax Requests: the htmx Approach

    One of the main selling points of htmx is that it gives developers the ability to send Ajax requests directly from HTML elements by utilizing a set of distinct attributes. Each attribute represents a different HTTP request method:

    • hx-get: issues a GET request to a specified URL.
    • hx-post: issues a POST request to a stated URL.
    • hx-put: issues a PUT request to a certain URL.
    • hx-patch: issues a PATCH request to a set URL.
    • hx-delete: issues off a DELETE request to a declared URL.

    These attributes accept a URL, to which they will send the Ajax request. By default, Ajax requests are triggered by the “natural” event of an HTML element (for example, a click in the case of a button, or a change event in the case of an input field).

    Consider the following:

    <button hx-get="/api/resource">Load Data</button>
    

    In the above example, the button element is assigned an hx-get attribute. Once the button is clicked, a GET request is fired off to the /api/resource URL.

    What happens when the data returns from the server? By default, htmx will inject this response directly into the initiating element — in our example, the button. However, htmx isn’t limited to this behavior and provides the ability to specify different elements as the destination for the response data. We’ll delve more into this capability in the upcoming sections.

    Triggering Requests with htmx

    htmx initiates an Ajax request in response to specific events happening on certain elements:

    • For input, textarea and select elements, this is the change event.
    • For form elements, this is the submit event.
    • For all other elements, this is the click event.

    Let’s demonstrate this by expanding our joke example from above to allow the user to search for jokes containing a specific word:

    <label>Keyword:
      <input
        type="text"
        placeholder="Enter a keyword..."
        hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode"
        hx-target="#joke-container"
        name="contains"
      />
    </label>
    
    <p id="joke-container">Results will appear here</p>
    

    To trigger the search, we need to fire the change event. For <input> elements, this occurs when the element loses focus after its value was changed. So type something into the box (such as “bar”), click elsewhere on the page, and a joke should appear in the <div> element.

    This is good, but normally users expect to have their search results updated as they type. To do this, we can add an htmx trigger attribute to our <input> element:

    <input
      ...
      hx-trigger="keyup"
    />
    

    Now the results are updated immediately. This is good, but it introduces a new problem: we’re now making an API call with every keystroke. To avoid this, we can employ a modifier to change the trigger’s behavior. htmx offers the following:

    • once: use this modifier if you want a request to be executed just once.
    • changed: this modifier ensures a request is only issued if the value of the element has been altered.
    • delay:<time interval>: this modifier sets a waiting period (like 1s) before the request is issued. Should the event trigger again during this waiting period, the countdown resets.
    • throttle:<time interval>: With this modifier, you can also set a waiting period (such as 1s) prior to issuing the request. However, unlike delay, if a new event is triggered within the set time, the event will be disregarded, ensuring the request is only triggered after the defined period.
    • from:<CSS Selector>: This modifier lets you listen for the event on a distinct element instead of the original one.

    In this case it seems that delay is what we’re after:

    <input
      ...
      hx-trigger="keyup delay:500ms"
    />
    

    And now when you type into the box (try a longer word like “developer”) the request is only fired when you pause or finish typing.

    See the Pen
    htmx Smart Search
    by SitePoint (@SitePoint)
    on CodePen.

    As you can see, this allows us to implement an active search box pattern in only a few lines of client-side code.

    Request Indicators

    In web development, user feedback is crucial, particularly when it comes to actions that may take a noticeable amount of time to complete, such as making a network request. A common way of providing this feedback is through request indicators — visual cues indicating that an operation is in progress.

    htmx incorporates support for request indicators, allowing us to provide this feedback to our users. It uses the hx-indicator class to specify an element that will serve as the request indicator. The opacity of any element with this class is 0 by default, making it invisible but present in the DOM.

    When htmx makes an Ajax request, it applies an htmx-request class to the initiating element. The htmx-request class will cause that — or any child element with an htmx-indicator class — to transition to an opacity of 1.

    For example, consider an element with a loading spinner set as its request indicator:

    <button hx-get="/api/data">
      Load data
      <img class="htmx-indicator" src="/spinner.gif" alt="Loading spinner">
    </button>
    

    When the button with the hx-get attribute is clicked and the request starts, the button receives the htmx-request class. This causes the image to be displayed until the request completes and the class is removed.

    It’s also possible to use an htmx-indicator attribute to indicate which element should receive the htmx-request class.

    Let’s demonstrate this with our Joke API example:

    <input
      ...
      hx-indicator=".loader"
    />
    
    <span class="loader htmx-indicator"></span>
    

    Note: we can grab some CSS styles for the spinner from CSS Loaders & Spinners. There are lots to choose from; just click one to receive the HTML and CSS.

    This will cause a loading spinner to displayed while the request is in flight.

    If we’re on a fast network, the spinner will only flash briefly when making the request. If we want to assure ourselves that it’s really there, we can throttle our network connection speed using our browser’s dev tools.

    Or, just for fun (that is, don’t do this on a real app), we could configure htmx to simulate some network latency:

    function sleep(milliseconds) {
      const date = Date.now();
      let currentDate = null;
      do {
        currentDate = Date.now();
      } while (currentDate - date < milliseconds);
    }
    
    document.body.addEventListener('htmx:afterOnLoad',  () => {
      sleep(2000);
    });
    

    This utilizes htmx’s event system, which we can tap into to modify and enhance its behavior. Here, we’re using the htmx:afterOnLoad event, which is triggered after the Ajax onload has finished. I’m also using a sleep function from a SitePoint article on the same subject.

    Here’s the completed demo. Type something into the box (such as “JavaScript”) then observe the loading indicator once the request is initiated.

    Targeting Elements & Swapping Content

    In some cases, we might want to update a different element than the one that initiated the request. htmx allows us to target specific elements for the Ajax response with the hx-target attribute. This attribute can take a CSS selector, and htmx will use this to find the element(s) to update. For example, if we have a form that posts a new comment to our blog, we might want to append the new comment to a comment list rather than updating the form itself.

    We actually saw this in our first example:

    <button
      hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode&type=single"
      hx-target="#joke-container"
    >
      Make me laugh!
    </button>
    

    Instead of the button replacing its own content, the hx-target attribute states that the response should replace the content of the element with an ID of “joke-container”.

    Extended CSS selectors

    htmx also offers some more advanced ways to select elements into which content should be loaded. These include this, closest, next, previous, and find.

    • The this keyword specifies that the element with the hx-target attribute is the actual target.
    • The closest keyword finds the closest ancestor of the source element that matches the given CSS selector.
    • The next and previous keywords find the following or preceding element in the DOM that matches the given CSS selector.
    • The find keyword locates the first child element that matches the given CSS selector.

    With reference to our previous example, we could also write hx-target="next p" to avoid specifying an ID.

    Content swapping

    By default, htmx will replace the content of the target element with the Ajax response. But what if we want to append new content instead of replacing it? That’s where the hx-swap attribute comes in. This attribute lets us specify how the new content should be inserted into the target element. The possible values are outerHTML, innerHTML, beforebegin, afterbegin, beforeend, and afterend. Using hx-swap="beforeend", for example, would append the new content at the end of the target element, which would be perfect for our new comment scenario.

    CSS Transitions with htmx

    CSS Transitions allow the smooth alteration of an element’s style from one state to another, without using JavaScript. These transitions can be as simple as a color change, or as complex as a full 3D transformation.

    htmx makes it easy to use CSS Transitions in our code: all we need to do is maintain a consistent element ID across HTTP requests.

    Consider this HTML content:

    <button hx-get="/new-content" hx-target="#content">
      Fetch Data
    </button>
    
    <div id="content">
      Initial Content
    </div>
    

    After an htmx Ajax request to /new-content, the server returns this:

    <div id="content" class="fadeIn">
      New Content
    </div>
    

    Despite the change in content, the <div> maintains the same ID. However, a fadeIn class has been added to the new content.

    We can now create a CSS transition that smoothly transitions from the initial state to the new state:

    .fadeIn {
      animation: fadeIn 2.5s;
    }
    
    @keyframes fadeIn {
      0% {opacity: 0;}
      100% {opacity: 1;}
    }
    

    When htmx loads the new content, it triggers the CSS transition, creating a smooth visual progression to the updated state.

    Using the View Transitions API

    The new View Transitions API provides a way to animate between different states of a DOM element. Unlike CSS Transitions — which involve changes to an element’s CSS properties — view transitions are about animating changes to an element’s content.

    The View Transitions API is a new, experimental feature currently in active development. As of this writing, this API is implemented in Chrome 111+, with more browsers expected to add support in the future (you can check its support on caniuse). htmx provides an interface for working with the View Transitions API, and falls back to the non-transition mechanism in browsers where the API isn’t available.

    In htmx, there are a couple of ways to use the View Transitions API:

    • Set the htmx.config.globalViewTransitions config variable to true. This will use transitions for all swaps.
    • Use the transition:true option in the hx-swap attribute.

    View Transitions can be defined and configured using CSS. Here’s an example of a “bounce” transition, where the old content bounces out and the new content bounces in:

    @keyframes bounce-in {
      0% { transform: scale(0.1); opacity: 0; }
      60% { transform: scale(1.2); opacity: 1; }
      100% { transform: scale(1); }
    }
    
    @keyframes bounce-out {
      0% { transform: scale(1); }
      45% { transform: scale(1.3); opacity: 1; }
      100% { transform: scale(0); opacity: 0; }
    }
    
    .bounce-it {
      view-transition-name: bounce-it;
    }
    
    ::view-transition-old(bounce-it) {
      animation: 600ms cubic-bezier(0.4, 0, 0.2, 1) both bounce-out;
    }
    
    ::view-transition-new(bounce-it) {
      animation: 600ms cubic-bezier(0.4, 0, 0.2, 1) both bounce-in;
    }
    

    In the htmx code, we use the transition:true option in the hx-swap attribute, and apply the bounce-it class to the content that we want to animate:

    <button 
      hx-get="https://v2.jokeapi.dev/joke/Any?format=txt&safe-mode" 
      hx-swap="innerHTML transition:true" 
      hx-target="#joke-container"
    >
      Load new joke
    </button>
    
    <div id="joke-container" class="bounce-it">
      <p>Initial joke content goes here...</p>
    </div>
    

    In this example, when the <div>‘s content is updated, the old content will bounce out and the new content will bounce in, creating a pleasing and engaging visual effect.

    See the Pen
    htmx View Transition API Demo
    by SitePoint (@SitePoint)
    on CodePen.

    Please keep in mind that, currently, this demo will only work on Chromium-based browsers.

    Form Validation

    htmx integrates well with the HTML5 Validation API and will prevent form requests from being dispatched if user input fails validation.

    For example, when the user clicks Submit, a POST request will only be sent to /contact if the input field contains a valid email address:

    <form hx-post="/contact">
      <label>Email:
        <input type="email" name="email" required>
      </label>
      <button>Submit</button>
    </form>
    

    If we wanted to take this a step further, we could add some server validation to ensure that only gmail.com addresses are accepted:

    <form hx-post="/contact">
      <div hx-target="this" hx-swap="outerHTML">
        <label>Email:
          <input type="email" name="email" required hx-post="/contact/email">
        </label>
      </div>
      <button>Submit</button>
    </form>
    

    Here we’ve added a parent element (div#wrapper) that declares itself as the recipient of the request (using the this keyword) and employs the outerHTML swap strategy. This means that the entire <div> will be replaced by the server’s response, even though it’s not the actual element triggering the request.

    We’ve also added hx-post="/contact/email" to the input field, which means that whenever this field is blurred, it will send a POST request to the /contact/email endpoint. This request will contain the value of our field.

    On the server (at /contact/email), we could do the validation using PHP:

    <?php
    // Get the email from the form submission
    $email = $_POST['email'];
    
    // Validate that the domain is "gmail.com" using regex
    $pattern = "/@gmail\.com$/i"; // Case-insensitive match for "@gmail.com" at the end of the email address
    $error = !preg_match($pattern, $email);
    
    // Sanitize email to avoid XXS
    $sanitizedEmail = htmlspecialchars($email, ENT_QUOTES, 'UTF-8');
    
    // Create error message if there's an error
    $errorMessage = $error ? '<div class="error-message">Only Gmail addresses accepted!</div>' : '';
    
    // Construct the HTML template, with conditional error message
    $template = <<<EOT
      <div hx-target="this" hx-swap="outerHTML">
        <label>Email:
          <input type="email" name="email" hx-post="/contact/email" value="$sanitizedEmail">
          $errorMessage
        </label>
      </div>
    EOT;
    
    // return the template
    echo $template;
    ?>
    

    As you can see, htmx is expecting the server to respond with HTML (not JSON) which it then inserts into the page at the specified place.

    If we run the above code, type a non-gmail.com address into the input, then make the input lose focus, an error message will appear below the field stating “Only Gmail addresses accepted!”

    Note: when inserting content into the DOM dynamically, we should also think about how a screen reader will interpret this. In the example above, the error message finds itself inside our label tag, so it will be read by a screen reader the next time the field receives focus. If the error message is inserted elsewhere, we should use an aria-describedby attribute to associate it with the correct field.

    It’s also worth noting that htmx fires a set of events around the validation process, which we can use to add our own validation logic and error handling methods. For example, if we wanted to implement the email check in JavaScript code, we could do this:

    <form hx-post="/contact">
      <label>Email:
        <input type="email" name="email" required>
      </label>
      <button>Submit</button>
    </form>
    
    <script>
      const emailInput = document.querySelector('input[type="email"]');
    
      emailInput.addEventListener('htmx:validation:validate', function() {
        const  pattern = /@gmail\.com$/i;
    
        if (!pattern.test(this.value)) {
          this.setCustomValidity('Only Gmail addresses accepted!');
          this.reportValidity();
        }
      });
    </script>
    

    Here, we’re using htmx’s htmx:validation:validate event, which is called before an elements checkValidity() method is called.

    Now when we try and submit the form with a non-gmail.com address, we’ll see the same error message.

    What Else Can htmx Do?

    htmx is a versatile library, built to boost the capabilities of HTML and provide a simple and powerful way of handling dynamic content updates in our web application. Its functionality extends beyond what has been outlined here, with features designed to give us a more interactive and responsive website without the complexity of heavy JavaScript frameworks.

    Before we wrap up, let’s have a quick look at some of these additional capabilities.

    Extensions

    Extensions are a powerful tool in the htmx toolbox. These customizable JavaScript components allow us to further augment and tailor the library’s behavior to our specific needs. Extensions range from enabling JSON encoding in requests, manipulating the addition and removal of classes on HTML elements, debugging elements, supporting client-side template processing, and more. With these at our disposal, we can customize htmx to a finer granularity.

    You can find a list of available extensions on the htmx site.

    Boosting

    htmx’s “boosting” functionality allows us to enhance standard HTML anchors and forms by transforming them into Ajax requests (akin to technologies like pjax from back in the day):

    <div hx-boost="true">
      <a href="/blog">Blog</a>
    </div>
    

    The anchor tag in this div will issue an Ajax GET request to /blog and swap the HTML response into the <body> tag.

    By leveraging this feature, we can create more fluid navigation and form submission experiences for our users, making our web applications feel more like SPAs.

    History management

    Speaking of SPAs, htmx also comes with built-in history management support, aligning with the standard browser history API. With this, we can push URLs into the browser navigation bar and store the current state of the page in the browser’s history, ensuring that the “Back” button behaves as users expect. This allows us to create web pages that feel like SPAs, maintaining state and handling navigation without reloading the entire page.

    Use with a third-party library

    One of the nice things about htmx is its ability to play well with others. It can integrate seamlessly with many third-party libraries, utilizing their events to trigger requests. A good example of this is the SortableJS demo on the htmx website.

    There’s also a confirm example which shows how to use sweetalert2 for confirmation of htmx actions (although this also uses hyperscript, an experimental frontend scripting language designed to be expressive and easily embeddable directly in HTML).

    Conclusion

    htmx is a versatile, lightweight, and easy-to-use tool. It successfully merges the simplicity of HTML with the dynamic capabilities often associated with complex JavaScript libraries, offering a compelling alternative for creating interactive web applications.

    However, it’s not a one-size-fits-all solution. For more complex applications, you may still find the need for a JavaScript framework. But if your goal is to create a snappy, interactive, and user-friendly web application without adding much complexity, htmx is definitely worth considering.

    As web development continues to evolve, tools like htmx provide exciting new ways to build better experiences for users. Why not give it a try on a future project and see what htmx can do for you?