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 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.

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. 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 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

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, he or she expects 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 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.

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 his place in the document and can continue browsing from there.

The menu:

The menu must have a wrapping <div> *(not* another <nav>) with the following properties:

  • role="dialog"
  • aria-modal="true"
  • 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 when the dialog is opened
  • optionally, 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 <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
  • Last element in the dialog
  • Typically given position: absolute to place it visually in the upper-right corner of the dialog
  • Again, must be a <button> element
  • If an icon is used for visual presentation, the button must have visually-hidden text, such as "Close" or "Close menu"
  • if an icon or letter "X" is used, it should be aria-hidden .

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

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

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. 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
  • Escape keypress
  • 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 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:

Add new comment

Plain text

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