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 ⭐️).