This is slightly adapted from presentation notes, so it's a bit sketchy. It's more of an outline of requirements, rather than code samples. I may add those later. Also, note that I've only covered two scenarios: a dropdown menu, and a menu that covers the entire screen when opened (modal dialog). There may be different considerations for other styles.
Mobile menu: usually the site's main menu, hidden until something is clicked, typically a "hamburger"-style icon. Usually displayed on smaller screens when there isn't space for a full menu, although some sites use this style at all screen sizes.
Different considerations for two different scenarios:
- A drop-down menu which displays directly under the toggle button, at its auto size. It does not cover the whole screen, though it's typically absolutely-positioned so it covers any elements under it
- A dialog-style menu which, when opened, covers the whole screen (or most of it with a semi-transparent overlay over the rest)
I'll first talk about the common requirements for both, then go into special requirements for the different scenarios.
Common mobile menu accessibility requirements
Menus must be contained in
<nav> elements, with visually-hidden
<h2> titles. Drupal does this very well in its
block--system-menu-block.html.twig template. You'll modify this template for your mobile menu, renaming it as needed.
The mobile menu trigger button must go *inside* the
nav (not before it), between the title and the menu itself.
nav and its title are how assistive tech users find the menu. They can then proceed to discover the button that opens it. A button floating by itself in non-semantic HTML space is not very discoverable.
Must be a
<button>. Not a link, div, span, or other element. Buttons natively announce themselves as such and handle click events. They are the correct element to perform actions on a page that are *not* navigating the user to another page, or another spot on the same page (that's what links are for).
Must contain text. The hamburger icon indicates to sighted users that it's a menu, but blind users can't see it; they need a label.
If the design doesn't include a visible label, you can either add
visually-hidden text or put an
aria-label on the button (the downside of
aria-label is it's not translatable, so I prefer
Text can be something like "Main menu", "Show main menu", "Open menu" (you can toggle show/hide or open/close with JS if it's a dropdown menu)
The icon should typically have the
aria-hidden="true" attribute, particularly if it's an SVG, an icon font icon, or unicode symbol. There are different ways to provide this icon but the bottom line is that (generally) it should be hidden from assistive tech and your meaningful label provides the button name.
Must be hidden from all users. This means giving it the CSS properties
opacity:0, zero height and width, positioning it off-screen, etc. (You can use those *in addition to* the first two for animation effects, if needed).
Why? If not, the menu is completely exposed to assistive tech users. They encounter a useless and misleading button, and the frustrating experience of having to navigate through the whole menu rather than being able to easily skip past it. For keyboard nav users, it's especially disorienting because they end up tabbing through invisible links and have no idea where they are.
Goal of accessibility: equivalent experience. If it's hidden for sighted/mouse users, it should be hidden for all users (with the exception of course of help text, like the heading which provides the menu's accessible name, which exists specifically for assistive tech)
You'll need to toggle these properties or the classes that define them with JS to show/hide.
visibility:hidden works best for CSS transitions/animations
- Menu displays on click of button
- Menu closes on click outside (for dialogs, if the menu covers the entire screen, this is irrelevant; if part of the screen is covered by an overlay, menu should close on overlay click)
- Menu closes on
This is the simplest in terms of accessibility requirements.
Button must have the
aria-expanded attribute, initially set to
false , toggling to
true when menu is opened and back to
false when it's closed
It is crucial that the menu comes *directly after the button* in the DOM order - when a screen reader user interacts with an
aria-expanded element, he or she expects to find the shown element *directly after* the toggle element.
Menu is opened by clicking the button, and closed by the following events:
- Clicking the button again
- Clicking outside the menu
This pattern is far more complicated. Modal dialogs have been called "the boss battle at the end of web accessibility".
But why a modal dialog? This isn't a dialog, it's a menu, right? If it's covering the whole screen when opened, it's a modal dialog, whatever its contents are.
Again, the goal with this pattern is *an equivalent experience*. When a modal dialog is open, a sighted/non-disabled/mouse- or finger-navigating user cannot see or interact with anything but the dialog. The same should be true for assistive tech users, it just takes a bit more work to make it happen.
There are a lot of tutorials out there on how to create an accessible mobile menu. I'm going to briefly go over the requirements, not exactly how to do all of them. I'll provide links to resources at the end.
Note: there are differences of opinion on some elements of modal dialog implementation, and technology and best practices change. This represents my current best understanding of how to build an accessible modal dialog.
The trigger button:
Does *not* have the aria-expanded attribute. Doesn't have anything special, other than I sometimes add text to the label indicating what will happen when it's clicked, e.g. "Main menu (opens dialog)".
Focus must be set back to the trigger button when the dialog is closed, so the user is returned to his place in the document and can continue browsing from there.
The menu must have a wrapping
<div> *(not* another
<nav>) with the following properties:
- You can either give it an
aria-labelwith the dialog name, or use
aria-labelledbyand a heading as the label (I'll go into that further). This is what screen readers announce when the dialog is opened
tabindex="-1"in order to focus the dialog programmatically when it's opened. SOMETHING needs to be focused, otherwise screen readers will say nothing. According to the official WCAG standard, it should be the first focusable element in the dialog. Others believe it should be the dialog itself, especially if the FFE comes after other content (which I agree with). In this case, it's a menu, so likely the first element *is* the FFE (a menu link), so you can set focus to that if you want.
- If you choose to go the title route to label the dialog, you can add a visually-hidden title as the first element
- Title heading level - there's some debate about this, I believe it should be
- Title needs an ID
- Finally, the wrapping dialog element has the
aria-labelledbyattribute pointing to the header:
aria-labelledby="[heading-id]"(value must match the heading ID exactly)
- Title heading level - there's some debate about this, I believe it should be
- Last element in the dialog
- Typically given
position: absoluteto place it visually in the upper-right corner of the dialog
- Again, must be a
- If an icon is used for visual presentation, the button must have
visually-hiddentext, such as "Close" or "Close menu"
- if an icon or letter "X" is used, it should be
When the button is clicked, the menu is shown (by setting
Either the dialog itself, or the first focusable element, is focused.
Every other element on the page, besides the modal dialog, is given the
inert property and set to
aria-hidden="true". This is so the virtual cursor (arrow keys, tab key, screen reader shortcut keys, etc) cannot interact with, perceive, or click any elements other than the dialog (in theory,
aria-modal="true" should do this, but it is not supported in some browsers).
Typically, I move the dialog when it's opened so it's the first child of the
<body>. I then set everything other than the dialog which is a direct child of
aria-hidden. These will usually only be a few top-level elements like the page wrapper, which is less of a performance hit than setting every single element. Child elements inherit these values.
When the dialog is open, a focus trap must be set so that when the last focusable element (LFE) of the dialog is focused and a user hits
Tab, they are taken to the first focusable element (FFE). If this doesn't happen, the tab will wander off into empty space.
According to the official WCAG spec, when the FFE is focused,
Shift + Tab should return focus to the LFE. A prominent accessibility specialist, Scott O'Hara, disagrees, saying it should be allowed to perform its normal function, which is to take the user to the browser chrome (browser address bar, buttons, menus, etc). FWIW I agree with him, but if you do handle
Shift + Tab that way, it is not incorrect.
The dialog should close on the following events:
- Click of the close button
- Click outside the modal, if it doesn't cover the entire window
When the dialog is closed:
- The button that opened it must be focused
- The dialog must again be set to
aria-hiddenproperties must be removed from the other elements
This is a really brief, galloping overview, but I hope it's given you enough of an idea to get started! Feel free to ask me questions anytime; I'm always happy to review code, do accessibility testing, or answer questions.