Tutorial
Create a Bootstrap Like Modal with Tailwind and Alpine.js
This post was updated 01 May - 2020
In certain scenarios, it doesn't really make sense to use LiveView. That can be toggling dropdowns, tabs, accordions, and opening modals. There is a great minimal library for that called Alpine.js.
So in this mini-tutorial, I will build a bootstrap 4-like animated modal using Tailwind markup and sprinkle some Alpine.js on it.
STEP 1 - Installation
Since I want to use the CSS transitions that come with Tailwind 1.2.0 I need to make sure I am on the latest version. And since that is not released yet, I can pull in the latest canary version. So I need to update my package.json
"tailwindcss": "^1.2.0-canary.5"
and install that version run
yarn
To install Alpine.js I will use yarn as well.
yarn add alpinejs
And in my app.js
add
import 'alpinejs'
STEP 2 - Implementation
I ended up giving the modal 3 states. CLOSED, TRANSITION, OPEN
The reason is that I couldn't get the animations working unless I added the extra transition step.
For example, regarding the backdrop. The modal has the initial state CLOSED
. When the button is clicked, I run the function open()
. That updates the state to TRANSITION
.
However, It has the initial opacity to 0% with opacity-0
. Then I have a setTimeout(() => { this.state = 'OPEN' }, 50)
that after 50ms changes states to OPEN
and by backdrop changes opacity to opacity-25
.
<!-- BACKDROP -->
<div
:class="{ 'opacity-25': isOpen() }"
class="z-40 fixed top-0 left-0 bottom-0 right-0 bg-black opacity-0 transition-opacity duration-200 linear"
></div>
And I solved the animation and fade-in on the modal with the same technique.
NOTE: If there is a better way to do this without the extra transition step, please let me know.
FULL CODE EXAMPLE
<div x-data="modal()">
<button x-on:click="open()" type="button" class="inline-block font-normal text-center px-3 py-2 leading-normal text-base rounded cursor-pointer text-white bg-blue-600" data-toggle="modal" data-target="#exampleModalTwo">
Launch modal
</button>
<!-- MODAL CONTAINER WITH BACKDROP -->
<div x-show="isOpening()">
<!-- MODAL -->
<div
:class="{ 'opacity-0': isOpening(), 'opacity-100': isOpen() }"
class="fixed z-50 top-0 left-0 w-full h-full outline-none transition-opacity duration-200 linear"
tabindex="-1"
role="dialog"
>
<!-- MODAL DIALOG -->
<div
:class="{ 'mt-4': isOpening(), 'mt-8': isOpen() }"
class="relative w-auto pointer-events-none max-w-lg mt-8 mx-auto transition-all duration-200 ease-out"
>
<!-- MODAL CONTAINER -->
<div class="relative flex flex-col w-full pointer-events-auto bg-white border border-gray-300 rounded-lg shadow-xl">
<div class="flex items-start justify-between p-4 border-b border-gray-300 rounded-t">
<h5 class="mb-0 text-lg leading-normal">Awesome Modal</h5>
<button
type="button"
class="close"
x-on:click="close()"
>×</button>
</div>
<div class="relative flex p-4">
...
</div>
<div class="flex items-center justify-end p-4 border-t border-gray-300">
<button
x-on:click="close()"
type="button"
class="inline-block font-normal text-center px-3 py-2 leading-normal text-base rounded cursor-pointer text-white bg-gray-600 mr-2"
>Close</button>
<button
type="button"
class="inline-block font-normal text-center px-3 py-2 leading-normal text-base rounded cursor-pointer text-white bg-blue-600"
>Save changes</button>
</div>
</div>
</div>
</div>
<!-- BACKDROP -->
<div
:class="{ 'opacity-25': isOpen() }"
class="z-40 fixed top-0 left-0 bottom-0 right-0 bg-black opacity-0 transition-opacity duration-200 linear"
></div>
</div>
</div>
<script>
function modal() {
return {
state: 'CLOSED', // [CLOSED, TRANSITION, OPEN]
open() {
this.state = 'TRANSITION'
setTimeout(() => { this.state = 'OPEN' }, 50)
},
close() {
this.state = 'TRANSITION'
setTimeout(() => { this.state = 'CLOSED' }, 300)
},
isOpen() { return this.state === 'OPEN' },
isOpening() { return this.state !== 'CLOSED' },
}
}
</script>