An accessible Alpine.js menu toggle
The “Hello World” of JavaScript frameworks and libraries seems to have become the todo app. In the case of Alpine.js a todo app is almost too large to showcase Alpine’s core benefits and use case.
Another issue with a lot of JavaScript examples is that they forego accessibility. Therefore we won’t be building a todo app but an accessible navigation menu.
Our menu is as follows (read on for a breakdown of how it’s built).
<nav
aria-labelledby="nav-heading"
x-data="{ isOpen: false }"
:aria-expanded="isOpen"
>
<h2 id="nav-heading">Alpine.js Accessible Navigation</h2>
<button
:aria-expanded="isOpen"
aria-controls="nav-list"
@click="isOpen = !isOpen"
>
See Alpine Resources
</button>
<ul :hidden="!isOpen" id="nav-list">
<li>
<a href="https://github.com/alpinejs/alpine">Alpine.js Docs</a>
</li>
<li>
<a href="https://github.com/alpinejs/awesome-alpine"
>Awesome Alpine.js list</a
>
</li>
<li>
<a href="https://alpinejs.codewithhugo.com/newsletter"
>Alpine.js Weekly Newsletter</a
>
</li>
</ul>
</nav>
The root element is a nav, Alpine.js state will get initialised to { isOpen: false } using x-data. Our menu contains a few links to Alpine.js resources.
<nav
x-data="{ isOpen: false }"
>
<ul>
<li>
<a href="https://github.com/alpinejs/alpine">Alpine.js Docs</a>
</li>
<li>
<a href="https://github.com/alpinejs/awesome-alpine"
>Awesome Alpine.js list</a
>
</li>
<li>
<a href="https://alpinejs.codewithhugo.com/newsletter"
>Alpine.js Weekly Newsletter</a
>
</li>
</ul>
</nav>
For accessibility purposes we bind aria-expanded to isOpen, this will mean the Alpine.js state of the nav will be reflected in the aria attribute. We also add a aria-labelledby whose value nav-heading is the id of our heading (h2).
<nav
aria-labelledby="nav-heading"
x-data="{ isOpen: false }"
:aria-expanded="isOpen"
>
<h2 id="nav-heading">Alpine.js Accessible Navigation</h2>
<!-- rest of the component -->
</nav>
To implement our toggle, we use a button with a click event listener (@click) which flips the isOpen boolean field (it sets it to false if it was true and true if it was false).
For accessibility we bind aria-expanded on the button to isOpen and use aria-controls on the button to signal the relationship between the button and the ul. aria-controls is set to nav-list which is the id we’ll set on the ul.
<nav
aria-labelledby="nav-heading"
x-data="{ isOpen: false }"
:aria-expanded="isOpen"
>
<!-- rest of the component -->
<button
:aria-expanded="isOpen"
aria-controls="nav-list"
@click="isOpen = !isOpen"
>
See Alpine Resources
</button>
<ul id="nav-list">
<!-- rest of the component -->
</ul>
</nav>
Finally, since isOpen controls the visibility of our navigation list, we’ll bind the hidden attribute the nav-list/ul to !isOpen, we want the nav-list to be visible (not hidden) when open and be hidden when not open.
<nav
aria-labelledby="nav-heading"
x-data="{ isOpen: false }"
:aria-expanded="isOpen"
>
<!-- rest of the component -->
<ul :hidden="!isOpen" id="nav-list">
<!-- rest of the component -->
</ul>
</nav>
The output of the component is as follows, on load, the nav-list is collapsed:

On click of “See Alpine Resources”, we see the 3 links.

That’s how you build an accessible navigation menu with Alpine.js.
You can find the examples for this post at Alpine.js Handbook Examples - 1.5 Accessible Menu
That’s it for this post, you can check out the Alpine.js tag on Code with Hugo for more in-depth Alpine.js guides.
If you’re interested in Alpine.js, Subscribe to Alpine.js Weekly. A free, once–weekly email roundup of Alpine.js news and articles.
Photo by Jordan Madrid on Unsplash
Interested in Alpine.js?
Power up your debugging with the Alpine.js Devtools Extension for Chrome and Firefox. Trusted by over 15,000 developers (rated 4.5 ⭐️).