A JavaScript-free Responsive Navigation Menu with Tailwind CSS

6 mins read

If you are building a static website using Tailwind CSS and looking for a responsive, collapsible navigation menu that is JavaScript-free, I might have something for you.

To create responsive navigation menus, it often involves using a “hamburger” menu for small screen widths and a regular navbar for larger screens. The conventional way of achieving a collapsible navigation menu is by attaching a JavaScript event handler to the hamburger menu to toggle the menu’s visibility on the click event.

However, we can achieve the same behavior by leveraging native HTML.

Simple Navigation Menu

Let’s look at a simple navigation menu styled with Tailwind CSS.

Demo

HTML

<nav class="flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800">
  <a href="#">Playground</a>
  <ul class="inline-flex gap-x-2">
    <li><a href="#" class="hover:underline">Home</a></li>
    <li><a href="#" class="hover:underline">About</a></li>
    <li><a href="#" class="hover:underline">Contact</a></li>
  </ul>
</nav>

Responsive Navigation Menu

Next, let’s add the hamburger menu for screens smaller than md using an HTML <button>.

Demo

HTML

<nav class="flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800">
  <a href="#">Playground</a>
  <div class="inline-flex gap-x-2 items-center">
    <button class="md:hidden rounded p-2 hover:bg-gray-100 dark:hover:bg-gray-700">
      <svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
          <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
      </svg>
    </button>
    <ul class="flex-col gap-y-2 hidden md:inline-flex md:flex-row md:gap-x-2">
      <li><a href="#" class="hover:underline">Home</a></li>
      <li><a href="#" class="hover:underline">About</a></li>
      <li><a href="#" class="hover:underline">Contact</a></li>
    </ul>
  </div>
</nav>

Usually, handling the button click event to toggle menu visibility requires JavaScript. Here, we are going to use HTML <label> and <input> elements to replace the <button>. According to HTML label element definition, the <label> element is designed to pass user interaction events to the associated <input> element via a matching id. We can utilize this design principle by placing the <input> tag anywhere in the HTML body.

Demo

HTML

<nav class="flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800">
  <a href="#">Playground</a>
  <input id="navbar-trigger" type="checkbox" class="hidden"/>
  <label for="navbar-trigger" class="md:hidden rounded p-2 hover:bg-gray-100 dark:hover:bg-gray-700">
    <svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
    </svg>
  </label>
  <ul class="flex-col gap-y-2 hidden md:inline-flex md:flex-row md:gap-x-2">
    <li><a href="#" class="hover:underline">Home</a></li>
    <li><a href="#" class="hover:underline">About</a></li>
    <li><a href="#" class="hover:underline">Contact</a></li>
  </ul>
</nav>

Tailwind CSS Peer Modifier

Next, we are going to use a CSS attribute selector on siblings to toggle menu visibility. This is where Tailwind CSS peer-{modifier} comes in handy.

This is the reason behind placing the <input> outside of the <label> to achieve the following hierarchy:

<input id="navbar-trigger"/>
<label for="navbar-trigger"></label>
<ul>
  <li>Home</li>
  <li>About</li>
  <li>Contact</li>
</ul>

Using peer-{modifiers}, we can now hide the <input> and add the peer/hamburger CSS class to allow the sibling <ul> CSS classes to bind with the <input> using peer-checked/hamburger:inline-flex.

Demo

HTML

<!-- adding flex-wrap to ensure expanded menu will be wrapped -->
<nav class="flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800">
  <a href="#">Playground</a>
  <input id="navbar-trigger" type="checkbox" class="hidden peer/hamburger"/>
  <label for="navbar-trigger" class="md:hidden rounded p-2 hover:bg-gray-100 dark:hover:bg-gray-700">
    <svg class="size-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
    </svg>
  </label>
  <!-- There are 2 styles for the <ul> -->
  <!-- Full width with border menu (< md): -->
  <!-- flex-col gap-y-2 mt-2 px-4 py-2 rounded-lg bg-gray-200 w-full hidden peer-checked/hamburger:inline-flex -->
  <!-- Auto-sized and borderless menu (>= md): -->
  <!-- md:w-auto md:inline-flex md:flex-row md:gap-x-2 md:mt-0 md:p-0 md:rounded-none md:bg-transparent -->
  <ul class="flex-col gap-y-2 mt-2 px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-700 w-full hidden peer-checked/hamburger:inline-flex md:w-auto md:inline-flex md:flex-row md:gap-x-2 md:mt-0 md:p-0 md:rounded-none md:bg-transparent">
    <li><a href="#" class="hover:underline">Home</a></li>
    <li><a href="#" class="hover:underline">About</a></li>
    <li><a href="#" class="hover:underline">Contact</a></li>
  </ul>
</nav>

And with that, you’ve created a fully functional, responsive navigation menu using Tailwind CSS, completely free of JavaScript, making it an ideal solution for lightweight, static websites.