CSS Transitions With Tailwind CSS and HTMX

Tailwind CSS and HTMX logos

Crocodile is a simple and fast web app for reviewing GitHub pull requests. You should check it out if you're fed up with slow UI, comments getting "outdated", and want a code review experience designed by and built for engineers. This post is about how we use Tailwind CSS and HTMX together in Crocodile to create CSS transitions.

Crocodile's frontend is built using Alpine.js, HTMX, and Tailwind CSS. We're going to look at how to make it easier to use HTMX and Tailwind together. If you've never heard of them, let me do a quick intro.

  • HTMX - Lets you make AJAX requests without writing JavaScript. You use HTMX by adding attributes to your HTML elements that specify what triggers the request, what endpoint to call, and what values to send. It also handles replacing page elements with any markup returned in the AJAX response.
  • Tailwind CSS - Tailwind dubs itself as a "utility-first" CSS framework. Instead of defining your styles in a separate CSS file, you add utility classes to your elements. No more agonizing over class names and trying to refactor styles so that they are reusable. All the classes are thoughtfully designed so that they work well together. I was a skeptic at first, but I've never been more productive with CSS.

HTMX adds and removes classes to DOM elements whenever it updates the page, which can be used to create CSS transitions. There are several examples on the HTMX website for how you can use the htmx-added, htmx-swapping, and htmx-added classes to create transitions.

Here's an example of deleting a row in a table.

<table hx-target="closest tr" hx-swap="outerHTML swap:1s">
  <tr>
    <td>
      <button hx-delete="/contact/1">Delete</button>
    </td>
  </tr>
</table>

If the /contact/1 endpoint responds to the DELETE request with an empty body, then HTMX will replace the containing <tr> element (specified via the hx-target attribute) with nothing, effectively removing it. During the page update process, HTMX will add the htmx-swapping class to the targeted <tr> element and wait for 1 second (specified by swap:1s in the hx-swap attribute) before deleting the row. To add a nice fade-out animation, you can add a little bit of CSS to animate the opacity over 1 second:

tr.htmx-swapping {
  opacity: 0;
  transition: opacity 1s ease-out;
}

But what if we're all in on Tailwind and want to stick with utility classes? It turns out it's possible by defining a plugin to add custom modifiers. Inside your tailwind.config.js file add a new plugin:

const plugin = require('tailwindcss/plugin')

module.exports = {
  // ...
  plugins: [
    plugin(function({ addVariant }) {
      addVariant('htmx-settling', ['&.htmx-settling', '.htmx-settling &'])
      addVariant('htmx-request',  ['&.htmx-request',  '.htmx-request &'])
      addVariant('htmx-swapping', ['&.htmx-swapping', '.htmx-swapping &'])
      addVariant('htmx-added',    ['&.htmx-added',    '.htmx-added &'])
    }),
  ],
}

This plugin lets you prefix any Tailwind utility class with htmx-settling:, htmx-request:, htmx-swapping:, or htmx-added: to conditionally style elements depending on whether the HTMX classes are present. The previous example can then be written like so:

<table hx-target="closest tr" hx-swap="outerHTML swap:1s">
  <tr class="htmx-swapping:opacity-0 transition-opacity duration-1000">
    <td>
      <button hx-delete="/contact/1">Delete</button>
    </td>
  </tr>
</table>

And here's how the fade in on addition example can be rewritten:

<button id="fade-me-in"
  class="htmx-added:opacity-0 opacity-100 transition-opacity duration-1000"
  hx-post="/fade_in_demo"
  hx-swap="outerHTML settle:1s">
  Fade Me In
</button>

That's it! By adding a few lines to our Tailwind config, we defined custom modifiers that we can use in combination with HTMX.

Author profile picture
James Lao