Psst. It would be super cool if you could try the new Phoenix Boilerplate!

Try now

Tutorial

Create a Bootstrap Like Modal with Tailwind and Alpine.js

This post was updated 01 May

alpinejsbootstrapmodaltailwind

In certain scenarios, it doesnt 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 comes 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 couldnt 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 rhe 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 top 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()"
            >&times;</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>

Sources

Alpine.js

Final Result


What are you working on?

If you want, you can send me a link to your Phoenix or Phoenix LiveView project so. Lets connect on Twitter or Linkedin.

- Andreas Eriksson, web developer since 2005

Related Tutorials

Published 31 Mar

Combine Phoenix LiveView with Alpine.js

liveviewphoenixtailwindalpinejs

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 11 Jul

Create a reusable modal with LiveView Component

tailwindalpinejsliveviewmodalphoenix

To reduce duplicity and complexity in your apps, Phoenix LiveView comes with the possibility to use reusable components. Each component can have its..