HTMX Tailwind Modal with Transitions
Enabling CSS Transitions in HTMX apps isn’t hard, but it wasn’t obvious to me. Hopefully this article will help someone (probably future me) grok it faster than I did.
HTMX is an appealing tool to me. It lets me write mostly server-side applications in a language I like and still have some dynamic sizzle on the front end where it’s useful. If you haven’t tried it out, I recommend taking a look.
One downside to HTMX is that there aren’t very many examples out there integrating it with Tailwind CSS. The easy stuff is easy but it can be a little challenging to get transitions working with Tailwind and HTMX.
Since I’ve spend some time figuring out how to build a modal with transitions based on HTMX and Tailwind, I thought I’d share my learnings with the world and describe what makes it work.
First off, check out the example at https://ryfow.github.io/htmx-tailwind-components. The code repository is at https://github.com/ryfow/htmx-tailwind-components.
Now that you’ve seen the modal work, lets see how it achieves that.
There are three pages to this example, modal_closed.html
, modal_open.html
, and index.html
.
modal_closed.html
modal_closed.html
is a version of the modal that is does not appear on the page.
It is adapted from the free modal example on tailwindui.com.
The z-index
of the top-level element is set to -50
, ids are set on elements that need CSS transitions, and the panel and background opacities are set to 0
.
1<!-- This example is adapted from https://tailwindui.com/components/application-ui/overlays/modal-dialogs -->
2<div id="modal" class="relative -z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
3 <div id="modal-background"
4 class="duration-300 opacity-0 fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
5 aria-hidden="true"></div>
6 <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
7 <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
8 <div id="modal-panel"
9 class="duration-300 opacity-0 translate-y-0 sm:scale-100 relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
10 <div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
modal_open.html
modal_open.html
is a version of the modal that is open. There are minimal differences with modal_closed.html
.
Element ids are the same, but the z-index
is set to 10
so the modal will appear above other UI elements.
Additionally, the opacity-100
class is added so the modal and modal background are visible.
1<!-- This example is adapted from https://tailwindui.com/components/application-ui/overlays/modal-dialogs -->
2<div id="modal" class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
3 <div id="modal-background"
4 class="duration-300 opacity-100 fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
5 aria-hidden="true"></div>
6 <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
7 <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
8 <div id="modal-panel"
9 class="duration-300 opacity-100 translate-y-0 sm:scale-100 relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
10 <div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
The following code listing is an output of running wdiff
on modal_close.html
and modal_open.html
.
As you can see, the differences are very minimal in z-index
and opacity
classes.
diff --git a/modal_closed.html b/modal_open.html index 6388e42..16fc5c7 100644 --- a/modal_closed.html +++ b/modal_open.html @@ -1,12 +1,12 @@ <!-- This example is adapted from https://tailwindui.com/components/application-ui/overlays/modal-dialogs --> <div id="modal" class="relative -z-10"z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true"> <div id="modal-background" class="duration-300 opacity-0opacity-100 fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div> <div class="fixed inset-0 z-10 w-screen overflow-y-auto"> <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <div id="modal-panel" class="duration-300 opacity-0opacity-100 translate-y-0 sm:scale-100 relative transform overflow-hidden rounded-lg bg-white tex <div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4"> <div class="sm:flex sm:items-start"> <div
index.html
index.html
does a few interesting things.
- Imports HTMX, Tailwind, and a DOM morphing library called Idiomorph (more on morphing later)
- Enables the Idiomorph extension with
hx-ext="morph"
- Loads the
modal_closed.html
file into the DOM with anhtmx-get="modal_closed.html" hx-trigger="load"
- Adds a button to run
hx-get="model_open.html"
withhx-swap="morph"
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>HTMX Tailwind Components</title>
5 <script src="https://cdn.tailwindcss.com"></script>
6 <script src="https://unpkg.com/[email protected]/dist/htmx.js"
7 integrity="sha384-BBDmZzVt6vjz5YbQqZPtFZW82o8QotoM7RUp5xOxV3nSJ8u2pSdtzFAbGKzTlKtg"
8 crossorigin="anonymous"></script>
9 <script src="https://unpkg.com/[email protected]/dist/idiomorph-ext.min.js"></script>
10 </head>
11 <body hx-ext="morph">
12 <div class="m-0 sm:m-4">
13 <div class="text-3xl">HTMX Tailwind Examples</div>
14 <div class="text-2xl my-10">Example Modal with Transitions</div>
15 <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" href="#" hx-swap="morph"
16 hx-get="modal_open.html" hx-target="#modal">Show Modal</button>
17 <div id="modal-container" hx-get="modal_closed.html" hx-trigger="load" hx-swap="innerHTML"></div>
18 </div>
19 </body>
20</html>
Opening the Modal with Transitions
When the user clicks the “Open Modal” button, an hx-get
is triggered to put the contents of modal_open.html
into the #modal
DOM element.
With a typical outerHTML
hx-swap
, the entire #modal
DOM element would get replaced.
The problem with this full replacement is that CSS transitions only work if they’re transitioning a specific DOM element from an old state to a new one.
Idiomorph resolves this by matching DOM id
values inside the new content with DOM id values in the old content, and “morphing” the elements rather than replacing them.
So, in addition to enabling Idiomorph as describe above, we set hx-swap="morph"
on our button
to enable Idiomorph when handling the response.
The result is that the opacity transitions smoothly when the dialog appears and disappears.
Summary
Adding CSS transitions to HTMX-resolved elements is fairly straightforward once you understand the appropriate combination of techniques.
- Pre-load elements that will need to be transitioned
- Add
id
attributes to elements that need to be transitioned - Use the
hx-swap="morph"
mechanism that Idiomorph enables to morph elements rather than replace them
Hopefully this example gets you started with using Tailwind CSS Transitions in your HTMX apps!