Building an Accessible Nav with Alpine.js and Tailwind CSS

Written By David Lindahl
Posted on

Some of the recent projects that I've been working on here at Zaengle don't need a fully-featured JavaScript framework like Vue.js or React, so I’ve been spending more and more time with Alpine.js. (Spoiler alert: Alpine is great!) Alpine is a nifty little JavaScript library for using JavaScript right in your markup/HTML. This is especially useful in situations where most of the content is getting rendered from a CMS in a templating language like Twig or Antlers (AKA when we are using Craft CMS or Statamic). In this post, I'll describe how I made a navbar using Alpine and Tailwind CSS for the new Laravel News website!

Laravel News was made in Statamic, so using Alpine.js made sense for those small areas where we needed a sprinkle of JavaScript - like the navbar's toggle open and close on mobile.

Here's what we did:

We initialized x-data on the <body> and set overflow-hidden if the navbar is open to prevent scrolling on the body:

<body class="flex flex-col min-h-screen antialiased"
  x-data="{ isMobileNavOpen: false }"
  :class="{ 'overflow-hidden': isMobileNavOpen }">

To toggle the navigation bar open, we used the Alpine method of x-on:click.prevent on a button with a dynamic aria-label depending on the state of the navbar:

<button x-on:click.prevent="isMobileNavOpen = !isMobileNavOpen"
              class="hover:text-white hover:bg-gray-700 focus:outline-none focus:bg-gray-700 focus:text-white z-20 inline-flex items-center justify-center p-2 text-gray-400 transition duration-300 ease-in-out rounded-sm"
              x-bind:aria-label="isMobileNavOpen ? 'Close main menu' : 'Main menu'"
              aria-label="Main menu"
              x-bind:aria-expanded="isMobileNavOpen">
      </button>

And then in the navbar itself we used x-show which toggles display: none on the element and added a little click listener so the navbar can be set to false if a user clicks outside the navbar:

<div class="md:relative md:w-auto md:h-auto md:bg-opacity-0 fixed top-0 left-0 z-20 flex flex-col items-center justify-center w-full h-full overflow-y-auto bg-black"
     x-show="isMobileNavOpen"
     @click.away="isMobileNavOpen = false">

Add in some Tailwind CSS animation classes and you have some fun and easy to use transitions! ✨

<div class="md:relative md:w-auto md:h-auto md:bg-opacity-0 fixed top-0 left-0 z-20 flex flex-col items-center justify-center w-full h-full overflow-y-auto bg-black"
     x-show="isMobileNavOpen"
     @click.away="isMobileNavOpen = false"
     x-transition:enter="transition ease-linear duration-200 transform"
     x-transition:enter-start="-translate-x-full"
     x-transition:enter-end="translate-x-0"
     x-transition:leave="transition ease-out duration-200 transform"
     x-transition:leave-start="translate-x-0"
     x-transition:leave-end="-translate-x-full">

Because we like to break out large files into smaller componentized files, I've got separate component files for the desktop and mobile navs.

Here's the parent file of both the mobile and desktop navbar, which includes the toggle button described above:

<nav class="max-w-screen-2xl relative z-20 w-full col-span-12 px-4 py-6 mx-auto">
  <!-- MOBILE NAV -->
  <div class="md:hidden z-30 flex items-center justify-between">
    <a class="flex items-center"
       href="/"
       aria-label="Home">
      {{ settings:site_logo }}
        {{ imgix:image_tag
          path="{{ path }}"
          class="w-12 h-12"
          :alt="alt"
        }}
      {{ /settings:site_logo }}
      <p class="font-display text-brand-500 ml-2 mr-4 text-xl font-bold leading-normal">{{ settings:site_name }}
      </p>
    </a>
    <div class="flex -mr-2">
      <button x-on:click.prevent="isMobileNavOpen = !isMobileNavOpen"
              class="hover:text-white hover:bg-gray-700 focus:outline-none focus:bg-gray-700 focus:text-white z-20 inline-flex items-center justify-center p-2 text-gray-400 transition duration-300 ease-in-out rounded-sm"
              x-bind:aria-label="isMobileNavOpen ? 'Close main menu' : 'Main menu'"
              aria-label="Main menu"
              x-bind:aria-expanded="isMobileNavOpen">
      </button>
    </div>
  </div>

  {{ partial:nav/header_desktop }}

  {{ partial:nav/header_mobile }}
</nav>

And here's the code for the child component mobile navbar:

<!-- MOBILE NAV OPEN -->
<div class="md:relative md:w-auto md:h-auto md:bg-opacity-0 fixed top-0 left-0 z-20 flex flex-col items-center justify-center w-full h-full overflow-y-auto bg-black"
     x-show="isMobileNavOpen"
     @click.away="isMobileNavOpen = false"
     x-transition:enter="transition ease-linear duration-200 transform"
     x-transition:enter-start="-translate-x-full"
     x-transition:enter-end="translate-x-0"
     x-transition:leave="transition ease-out duration-200 transform"
     x-transition:leave-start="translate-x-0"
     x-transition:leave-end="-translate-x-full">
  <button @click.prevent="isMobileNavOpen = !isMobileNavOpen"
          class="hover:text-white hover:bg-gray-700 focus:outline-none focus:bg-gray-700 focus:text-white absolute top-0 right-0 z-50 inline-flex items-center justify-center p-2 m-4 text-gray-300 transition duration-200 ease-in-out rounded-sm"
          x-bind:aria-label="'Close main menu'"
          aria-label="Main menu"
          x-bind:aria-expanded="isMobileNavOpen">
  </button>
  <div class="flex flex-col items-center w-full h-full p-12">
    <div class="pt-8 mb-6">
      <a class="flex items-center"
         href="/"
         aria-label="Home">
        {{ settings:site_logo }}
          {{ imgix:image_tag
            path="{{ path }}"
            class="w-12 h-12"
            :alt="alt"
          }}
        {{ /settings:site_logo }}
        <p class="font-display text-brand-500 ml-2 text-xl font-bold leading-normal">
          {{ settings:site_name }}
        </p>
      </a>
    </div>

    {{ partial:components/search location_a11y="header_mobile" }}

    <div class="flex-grow w-full max-w-lg">
      <ul class="grid grid-cols-12 gap-2 py-8">
        {{ nav:main_header }}
        <li class="inline-flex items-center justify-center col-span-12">
          {{ partial:components/link 
              class="link--gray link--lg mx-3 mt-3 mb-6 text-4xl font-bold tracking-tight" 
              :text="title"
              :href="url"
            }}
        </li>
        {{ /nav:main_header }}
      </ul>
    </div>

    <div class="flex flex-col justify-center w-3/4">
      <div class="flex justify-center my-6 space-x-8">
        {{ social_media_links:social_media_links }}
        <a href="{{ link }}"
           target="_blank"
           class="hover:rotate-12 transition-transform duration-100 ease-linear transform"
           aria-label="{{ name }} link">
           {{ icon }}
            {{ imgix:image_tag
              path="{{ path }}"
              class="h-5 text-white fill-current"
              :alt="alt"
            }}
           {{ /icon }}
        </a>
        {{ /social_media_links:social_media_links }}
      </div>
      <ul class="flex items-center justify-center space-x-12">
        {{ nav:secondary_header }}
        <li>
          {{ partial:components/link 
            class="link--gray font-display text-sm tracking-wider uppercase" 
            :href="url"
            :text="title" 
          }}
          {{ /nav:secondary_header }}
        </li>
      </ul>
    </div>
  </div>
</div>

Accessibility

You may have noticed aria-labels included in our code snippets. These aria-labels are important for accessibility because they help everyone, including those folks who use screen readers, gain full context of the situation. Here’s where we added them to our navigation:

  • Navigation links using conditional titles in antlers
  • The logo link that appears in the top left that leads a user to home upon click
  • Navbar itself to dynamically indicate via alpine its status (open or closed)

You can read more about our Accessibility Guidelines for Development here.

Tailwind and Alpine make a great combination. I like being able to see everything (the styles and the javascript) in one single source of truth and using Tailwind's transition classes in the Alpine is an easy way to get nice animations!