This might be the first time you’re working on a component with screen readers. We’ll take it slow; I’ll walk you through every step of the way.
The first thing we want to do is audit what we’ve built with a screen reader.
The menu button
When Voiceover reaches the menu button, it says “Menu, Button”. This informs the user they’re on a <button> element with text that says “Menu”.
The button doesn’t say whether the menu is opened or closed. We can add this context by adding aria-expanded="false" into the HTML.
Now Voiceover reads “Main navigation, navigation”.
Navigating through the menu
Most screen readers users will use the Tab key to get through the navigation (since navigations contain links). Here’s what it looks like with Voiceover.
In this case, Voiceover says “Link” for each item. This “Link” comes from the link role, which is built into <a> with a href attribute.
This is why we need to use the right elements for the right job.
They may also use the “Next item” shortcut.
Command
Shortcut
Next item (Voiceover)
VO + →
Next item (NVDA)
↓
There’s not much difference between VO + → and Tab on Safari.
If you use NVDA, you’ll notice it says:
“Item, Link” when you press Tab
“Link, Item” when you press ↓
No biggie here. We can’t do anything about this small NVDA quirk.
Closing the menu
We need a way to let screen reader users close the menu. And we have one—the Escape key—but they don’t know it exists.
We can tell users they can use the escape key to close the navigation.
<nav class="nav" tabindex="-1" aria-label="Main navigation">
<ul> <!-- ... --> </ul>
<p>Press Escape to close this navigation.</p>
</nav>
We want to hide this piece of text from visually from non-screen reader users. The best way to hide text is with this CSS:
After adding the close menu button, we need to correct our JavaScript to use the correct button elements.
// Change this
const button = document.querySelector('button')
// To this
const menuButton = document.querySelector('.menu-button')
// Make sure you change `button` to `menuButton` in the rest of the code.
// ...
We also need to add an event listener to close the menu.
When we close the navigation, we need to set aria-expanded on the menu button back to false. This would tell screen reader users the navigation is collapsed.
Blind users cannot see the off-canvas menu. When they activate the menu button, we bring them to the <nav> element with the focus method. This makes them think the <nav> is directly after the menu button.
<!-- What a screen reader user believes the HTML to be -->
<button class="menu-button">
<nav aria-label="Main navigation"> ... </nav>
In this case, it makes sense to bring focus back to the menu button if they press Shift + Tab on <nav>,
To do this, we need to create an event listener that listens for Shift + Tab.
document.addEventListener('keydown', event => {
if (event.key !== 'Tab') return
if (!event.shiftKey) return
})
If the event target is <nav>, we return focus to the menu button.
Users may Tab into the first element (the close button) before pressing Shift + Tab. In this case, it makes sense to bring the user back to the menu button as well.
document.addEventListener('keydown', event => {
if (event.key !== 'Tab') return
if (!event.shiftKey) return
if (event.target === menu || event.target === closeButton) {
event.preventDefault()
menuButton.focus()
}
})