How to create a dropdown using Alpine.js

27 Jan 2020

I’m going to use tailwindcss to style the component, since css isn’t really going to be the focus of this post and tailwindcss is what I use for pretty much everything these days :)

Here’s a JSFiddle with the finished result if you feel like checking that out before diving in. The HTML will be a div that shows the name of the user and when you click on that a menu will slide down which we’ll use a ul for.

<div class="relative inline-block">
  <button class="flex inline-block p-2 pl-3 pr-1 text-gray-700 bg-gray-100 border border-gray-400 rounded shadow cursor-pointer focus:outline-none hover:text-black">
    John Doe
  </button>

  <ul class="absolute w-40 py-1 mt-2 text-indigo-600 bg-white rounded shadow">
    <li><a href="#" class="block px-3 py-1 hover:bg-indigo-100">Profile</a></li>
    <li><a href="#" class="block px-3 py-1 border-b hover:bg-indigo-100">Billing</a>
    <li><a href="#" class="block px-3 py-1 hover:bg-indigo-100">Log out</a></li>
  </ul>
</div>

That’s our basic structure of the dropdown styled with tailwindcss to give it the look we want.

Let’s add interactivity

Next step is to spice up the boring example above with some alpine.js sprinkle, so we can open and close the dropdown. We do that by adding some state in form of a simple javascript object to the outer-most div:

<div class="relative inline-block" x-data="{ open: false }">

Now we only want to show the ul if open is true, so on our ul element we add x-show="open" like so:

<ul x-show="open" ...>

Now the ul will be visible when open is true and display: none otherwise. But.. but how do we toggle the open state? Easy enough, we’ll add an event listener to the button like so:

<button @click="open = !open" ...>
  John Doe
</button>

Now when you click the button we set the state of open to the inverse, easy!

Transition time!

We could basically end the tutorial here, but let’s add a few other things such as an active state to the “button” and transitions to the ul so it slides in when we open it and slides away when we close it.

First for the boring part, which is an active state to the button - first we add an arrow that points down when the dropdown is closed. Add this after John Doe inside of the button element:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="inline-block w-6 h-6 ml-1 text-gray-500 fill-current"><path fill-rule="evenodd" d="M15.3 10.3a1 1 0 011.4 1.4l-4 4a1 1 0 01-1.4 0l-4-4a1 1 0 011.4-1.4l3.3 3.29 3.3-3.3z"/></svg>

We want to rotate this svg when the dropdown is open, so we want to add a class that rotates it 180 degrees when open is true we can do that using the :class attribute that looks like this:

:class="{'rotate-180': open}" 

That’ll make the SVG element look like so:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" :class="{'rotate-180': open}" class="inline-block w-6 h-6 ml-1 text-gray-500 fill-current transform"><path fill-rule="evenodd" d="M15.3 10.3a1 1 0 011.4 1.4l-4 4a1 1 0 01-1.4 0l-4-4a1 1 0 011.4-1.4l3.3 3.29 3.3-3.3z"/></svg>

Notice how we’ve also added a transform class to the SVG (this comes with the latest canary of tailwindcss) and is required for us to apply the rotation via the rotate-180 class.

Time for the transitions!

Alpine.js comes with a x-transition directive (see documentation), more specifically the following:

:enter - which is applied during the entering phase :enter-start - added before the element is inserted and removed one frame after insertion :enter-end - added one frame efter element is inserted and removed when the transition/animation finishes :leave - applied during the leave phase :leave-start - Added immediately when a leaving transition is triggered, removed after one frame. :leave-end - Added one frame after a leaving transition is triggered and removed when the transition/animation finishes

If you have played with Vue.js this might ring a bell, but basically this allows us to apply css-classes during different parts of the lifecycle when an element is shown/hidden in the DOM.

Go ahead and add the following to the ul element:

x-transition:enter="transition-transform transition-opacity ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-end="opacity-0 transform -translate-y-3"

In the enter phase we apply the transition class so we can transition transform and opacity, we set the duration to 300ms and we use the ease-out timing function that tailwind supplies. Right before we show the element we shift the element up the Y-axis a little bit using -translate-y-2 and set the opacity to 0 - and we then transition it down to where it was origianlly on the Y-axis by setting the tranlate-y-0 class - that’ll cause the dropdown to fade from opacity 0 to 100 and slide down from the top to the bottom.

We want to do a similar thing for when we hide it. For that we use leave-end which we set to opacity 0 and move back up the Y-axis so it’ll slide form where it is and up a few pixels.

That is our transitions! Let’s add one more thing to the button, when it’s pressed we want the shadow to disappaer and the text to be semi-bold, so add the following using :class once again:

:class="{'font-semibold': open, 'shadow-none': open}"

That makes the button open tag look like this:

<button @click="open = !open" class="..." :class="{'font-semibold': open, 'shadow-none': open}">

Voila! There’s your dropdown :) With relatively little Alpine.js attributes we’ve managed to make a fully functioning dropdown. Pretty nifty! Of course there’s a lot of stuff missing like accessibility, keyboard support etc - but let’s cover that in a later post!