How To Create Toggle-able Menus in Tailwind CSS

No JS or Additional CSS Required.

A toggle button, showing the tailwind css logo in the 'enabled' position
tl;dr

Use <input type='checkbox'> + the peer and peer-checked: tailwind utility classes.

°l||l°l||l°l||l°l||l°l||l°l||l°l||l°l||l°l||l°l||l°

Tailwind CSS is fantastic and will probably continue to become more and more popular than it already is. Nonetheless, there's a lot of HTML/CSS help on the internet out there, and hardly any of it is Tailwind specific. So, here's how you can easily create toggle-able/collapsable menus using just tailwind.

We'll just use a basic example, but the ideas are universal and very easy to apply to virtually any scenario you'd like to use.

Create Your Menu's Layout🔗

We start by creating our html the way that it will look in it's toggled (i.e. open) state.

In this example, I'm going to create a nav element, with a flexbox layout to hold everything. You can do this any way you like, but what's important here is that - however you do it - anything we want to toggle must be siblings of each other, i.e. nested the same way in our html.

So, because I want to toggle my menu and I'm using a nav element, I will make my menu a child of nav, even though it's position is fixed. Also, the menu uses fixed because when it's open I want it to cover the content.

<nav class="h-16 px-2 flex items-center text-3xl bg-red-400">
  <span>Menu</span>
  <div id="menu" class="fixed top-16 right-0 bg-blue-400">
    <ul class="h-screen text-2xl py-2 px-4">
      <li>Link 1</li>
      <li>Link 2</li>
      <li>Link 3</li>
      <li>Link 5</li>
      <li>Link 5</li>
    </ul>
  </div>
</nav>

Here's the example above in the tailwind playground.

(Note: there's some additional styling here just to make everything be a little more realistic. Mainly the height, padding, items alignments, and text size.)

todo

Add a Checkbox + Labels as a Siblings to Your Menu🔗

Now that our layout is in place, we need a way to control it. To do that we're going to need make use of an <input type='checkbox'> and two tailwind utilities: peer and peer-checked:. As alluded to before, the <input> element is going to be a sibling to what we want to control. This is necessary when using peer + peer-checked:.

Now, we're going to use peer on our <input>, which designates it as the thing that will do the controlling. Then, we give peer-checked: to whatever we want to control. We also want our menu to be hidden until a user taps "Open", so we need a default "off" state. For that we'll use hidden for now.

As you can see in the example below, we're hiding both the "Close" labels and our menu by default, but displaying both when the "Open" label is tapped.

Note: We can hide the actual <input> element and use <label> to toggle its state.

<nav class="h-16 px-2 flex items-center text-3xl bg-red-400">
  <span>Menu</span>

  <input type="checkbox" class="peer hidden"
    name="toggle-menu" id="toggle-menu" />

  <label class="ml-auto peer-checked:hidden"
    for="toggle-menu">Open</label>

  <label class="ml-auto hidden peer-checked:inline"
    for="toggle-menu">Close</label>

  <div id="menu" class="hidden
    fixed top-16 right-0
    peer-checked:block
    ">
    <ul class="h-screen text-2xl py-2 px-4 bg-blue-400">
      <li>Link 1</li>
      <li>Link 2</li>
      <li>Link 3</li>
      <li>Link 4</li>
      <li>Link 5</li>
    </ul>
  </div>
</nav>

Here's the example above in the tailwind playground.

todo

As you can see we have one label to open the menu, and one to close it.

So in summary the "Open" <label> is shown by default, and it gets toggled off/on when tapped, whereas the "Close" <label> and our menu, i.e. <div id="menu"> are the reverse and get toggled on/off. The <input> itself is hidden all the time, we don't need to show it to make use of it.

Add Animation (Optional)🔗

Finally, we want add some animation and the ability to tap outside the the menu to close it, instead of always having to always tap "Close". To do that we're going to add a third label, that acts just like our "Close" label from above, but it will be transparent and take up all remaining space outside of the element.

Additionally we have to implement our "off" state differently now. We can't use peer-checked:block to hide the menu and use animations, since CSS doesn't support animating transitions of the display property. To do this, we're just going to set the off state to literally off the screen, and then our on state will bring it back. So let's see what the final code looks like.

<nav class="h-16 px-2 flex items-center text-3xl bg-red-400">
  <span>Menu</span>

  <input type="checkbox" class="peer hidden"
    name="toggle-menu" id="toggle-menu" />

  <label class="ml-auto peer-checked:hidden cursur-pointer"
    for="toggle-menu">Open</label>

  <label class="ml-auto hidden peer-checked:inline cursor-pointer"
    for="toggle-menu">Close</label>

  <div id="menu" class="w-full h-screen flex
    fixed top-16 -right-full
    peer-checked:-translate-x-full
    transform-gpu transition-transform duration-200 ease-in-out
    ">
    <label class="ml-auto flex-grow" for="toggle-menu"></label>
    <ul class="h-screen text-2xl py-2 px-4 bg-blue-400">
      <li>Link 1</li>
      <li>Link 2</li>
      <li>Link 3</li>
      <li>Link 4</li>
      <li>Link 5</li>
    </ul>
  </div>
</nav>

Here's a link to the final version.

todo

Probably the trickiest thing to get here is how the menu is hidden when our toggle is "off". Basically, we have a div that's set to w-full and its display is set to flex. We then put that entire thing off screen with fixed top-16 -right-full. When its peer gets checked, we perform a CSS translate animation using Tailwind, i.e. peer-checked:-translate-x-full, and then the rest of the class info is just stuff to make it look nicer, i.e. transform-gpu transition-transform duration-200 ease-in-out.

The last thing I need to say about this is there's also the <label> next to the <ul>. That's our new, third <label> which we use to implement clicking outside of the menu to close it. As you can see it has a tailwind class of flex-grow which instructs it to basically take up any available space. So, the browser is going to first allocate space to the menu, because of the menu's content. After that it'll go to whomever is using flex-grow.

Hope that helps, let me know what you made.