Theme colour switcher

How to create an accessible mobile menu

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 use aria-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)
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)

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 or visibility:hidden
  • The inert and aria-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

Tags:

Comments

Love your site! Thank you for sharing.

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.