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
Markup
Menus must be contained in <nav>
elements, with unique accessible labels for each nav on the page if there is more than one. 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.
Why? The 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.
The button:
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 visually-hidden
).
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.
The menu:
Must be hidden from all users until it is opened. This means giving it the CSS properties display:none
or visibility:hidden
. Not 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 sighted keyboard 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
Behaviour
- 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
Escape
key press
Dropdown menu
This is the simplest in terms of accessibility requirements.
Markup
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, they expect to find the shown element *directly after* the toggle element.
Behaviour
Menu is opened by clicking the button, and closed by the following events:
- Clicking the button again
Escape
keypress- Clicking outside the menu
Dialog menu
This pattern is 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 mouse or touch user cannot see or interact with anything but the dialog. The same must be true for assistive technology, 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 modals. 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.
Markup
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 the same place in the document and can continue browsing from there. If not, they have to start over again at the top of the page.
The menu:
The menu must have a wrapping <div>
*(not* another <nav>
) with the following properties:
role="dialog"
- Optionally,
aria-modal="true"
. This one doesn't have great support yet, and we still have to compensate for it, so it might as well be left off. - You can either give it an
aria-label
with the dialog name, or usearia-labelledby
and a heading as the label (I'll go into that further). This is what screen readers announce as the dialog name when it is opened - 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
<h2>
- Title needs an ID
- Finally, the wrapping dialog element has the
aria-labelledby
attribute 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
Close button
- Can either be the first or last element in the dialog, in the DOM order
- Again, must be an HTML
<button>
element - If an icon alone is used for visual presentation and the button has no visible text label, it must have a
visually-hidden
label, such as "Close" or "Close menu"- if an
<img>
is used as a button graphic, the alt text can be used as the button label) - If an SVG is used for the graphic, I tend to
aria-hide
the SVG and provide visually-hidden text rather than labelling the SVG (just personal preference; SVG text label support is somewhat uneven. You can give them an aria-label but beware of translation issues if you do that)
- if an
Behaviour
This is where things get complicated. This behaviour is achieved with JavaScript.
When the button is clicked, the menu is shown (by setting display:block
or visibility:visible
).
Focus is taken to the dialog, for users navigating by keyboard and/or screen reader. The official recommendation is to focus the first focusable element in the dialog, which in our case is the first menu link.
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 fully supported yet).
inert
requires a polyfill: [https://github.com/WICG/inert](https://github.com/WICG/inert). Inert
doesn't take any values (no true/false or other); e.g. <div class="something" inert>
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 body
to inert
and 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. Some accessibility experts disagree and say it should be allowed to take the user to the browser toolbar, which I personally agree with.
The dialog should close on the following events:
- Click of the close button
Escape
keypress- Many modals close on mouse click outside. Some accessibility experts disagree with this, saying it's too easy for users with motion disabilities to click outside by accident. I don't personally implement this functionality.
When the dialog is closed:
- The button that opened it must be focused
- The dialog must again be set to
display:none
orvisibility:hidden
- The
inert
andaria-hidden
properties 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.
Resources
- https://robdodson.me/building-better-accessibility-primitives
- https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-m…
- https://www.scottohara.me/blog/2019/03/05/open-dialog.html
- https://www.scottohara.me/blog/2016/09/07/revised-modal-window.html
- https://bitsofco.de/accessible-modal-dialog
Comments
Love your site! Thank you for sharing.