Accessible Flyout/Hamburger Hybrid Menus

This is the best solution to use, especially when building from scratch.
If you are trying to fix an existing menu system, please go through the the code walkthrough of how this was implemented.
This solution described below is available as an NPM module. (Module installation instructions)

This is the component that the most development and testing time was spent on. On many sites I have done accessibility audits on, there is a main navigation that appears as a traditional flyout menu on the desktop breakpoint and a mobile hamburger menu on the tablet and mobile breakpoints. More often than not, this component would have several accessibility issues:

  1. On the desktop, when the user opened a menu flyout and tabbed through the flyout to the next flyout button, the flyout wouldn't close.
  2. On mobile, there wouldn't be a focus loop around the hamburger menu when opened.
  3. On mobile, when opening up a submenu, the focus wouldn't go the the back button/close button of the new submenu.
  4. Links and collapsable buttons were not marked up correctly.

I created this menu system to address all of the above issues. I have tested with both mobile and desktop devices with and without screen readers. The visual layout of the mobile breakpoint is inspired by this hamburger menu by the talented Hayley Tong, the code running it is original work.

Mobile Hamburger Menu

If you are in the mobile breakpoint (i.e. a viewport width less than ), then a hamburger menu icon will appear in the upper right-hand corner of this page.

Screenshot of the banner on the top of this page in the mobile breakpoint
Figure 1. The hamburger menu icon appears on the upper right-hand side of the page. It is denoted by three horizontal lines that has become the standard.

Clicking on it with either a mouse or keyboard will result in a standard hamburger menu appearing on the right hand side of the page, and keyboard focus will be applied to the first interactive element inside it (the close button).

Screenshot of the hamburger menu when opened.
Figure 2. When the hamburger menu icon is clicked, the black menu above appears. It has a close button (that gains keyboard focus when first opened) and a few CTAs stacked on top of each other.

The user can choose any item inside that menu with either a mouse or keyboard. Menu subcategories are visually indicated by a right-pointing chevron, and to assistive technologies as collapsible/expandable buttons. Clicking on these subcategory buttons will show the subcategory menu appearing, with keyboard focus being applied to the back button that will take users back to the previous menu.

Keyboard users experience a focus loop that keeps the current menu panel open until the menu is closed. If the user either uses a mouse to click outside the menu or hits the Escape key, the menu will close.

Desktop Mega Menu

If you are in the desktop breakpoint (i.e. a viewport width greater than or equal to ), then a mega menu will appear across the top of the page underneath the Enable logo in the global header.

Screenshot of the mega menu when the page is first loaded.
Figure 3. The mega menu is a horizontal bar with the top-level CTAs appearing inside it next to one another.

Users can either click the CTAs in the menu with either a mouse or keyboard. Menu subcategories are visually indicated by a downwards pointing chevron, and to assistive technologies as collapsible/expandable buttons. Clicking on these subcategory buttons will show the subcategory menu appearing below the button. Keyboard users can then tab immediately into the subcategory menu with a keyboard, while mouse users can click on any of the submenu items inside.

Screenshot of the mega menu when one of the submenus opened.
Figure 3. When a submenu category is clicked with either, a mouse or keyboard, the submenu will appear. Clicking again makes it disappear.

Keyboard users will note that when they apply focus to an interactive element outside of the subcategory menu, the menu will close automatically. Mouse users will notice this happening if they click anywhere outside the subcategory menu as well. Mobile screen reader users will experience a focus loop inside the menu until they close the menu with the CTA that opened it.

How can I use this script on my site?

  1. Follow the instructions below to learn how to download the hamburger menu library.
  2. Use the following code walkthrough below to create your menu navigation.
☜ Scroll to read full source ☞


So ... What Makes This Accessible

Let's walk through the front-end code of the Hamburger Flyout Menu on the Enable site to show the code that makes this accessible.

☜ Scroll to read full source ☞


Installation Instructions

You can load this JavaScript library into your application in several ways:

If you haven't done so already, choosing which you should use is a major architectural decision. Here are a few articles that will help you decide:

Important Note on the CSS Classes Used in This Module:

This module requires specific CSS class names to be used in order for it to work correctly. These CSS classes begin with enable-flyout__. Please see the documentation above to see where these CSS classes are inserted.

Using NPM/Webpack to load ES6 Modules:

  1. Install the enable-a11y NPM project.
  2. Edit your webpack.config.json file to resolve the ~ modifier by adding the following:
    ☜ Scroll to read full source ☞
    module.exports = { ... resolve: { extensions: ['.js', '.jsx', '.scss', '.css', '*.html'], modules: [ path.resolve('./src/js'), path.resolve('./node_modules') ], alias: { '~enable-a11y': path.resolve(__dirname, 'node_modules/enable-a11y') }, ... }, ... }
  3. You can use the module like this:
    ☜ Scroll to read full source ☞
    // import the JS module import enableFlyout from '~enable-a11y/js/modules/enable-flyout';
    // import the library that converts JSON to HTML
    import Templify from "~enable-a11y/js/modules/templify.js"
    // import the CSS for the module import '~enable-a11y/css/enable-flyout'; // How to initialize the enableFlyout library enableFlyout.init();
    // This is the DOM element where the hamburger menu will be inserted into.
    const hamburgerMenuEl = document.getElementById('enable-flyout-menu');

    // This is where the structure of the hamburger menu is stored (in JSON format).
    const hamburgerMenuJSONEl = document.getElementById('flyout-props');
    const hamburgerMenuJSON = JSON.parse(hamburgerMenuJSONEl.innerHTML);

    // Now, let's use Templify to convert the JSON into HTML.
    const hamburgerMenu = new Templify(hamburgerMenuEl, hamburgerMenuJSON);

    // Initialize the hamburger menu.
  4. Alternatively, if you are using LESS you can include the styles in your project's CSS using:
    ☜ Scroll to read full source ☞
    @import '~enable-a11y/css/enable-flyout';
    (If you are using it in your CSS, you will have to add the .css suffix)

Using NPM/Webpack to Load Modules Using CommonJS Syntax

  1. Install the enable-a11y NPM project.
  2. You can import the module using require like this:
    ☜ Scroll to read full source ☞
    var enableFlyout = require('enable-a11y/enable-flyout').default; ... enableFlyout.init();
  3. You will have to include the CSS as well in your project's CSS using:
    ☜ Scroll to read full source ☞
    @import '~enable-a11y/css/enable-flyout';

Using ES6 modules natively.

This is the method by which the page you are reading now loads the scripts.

  1. Grab the source by either using NPM, grabbing a ZIP file, or cloning the enable source code from GitHub.
  2. If you want to load the module as a native ES6 module, copy js/modules/enable-flyout.js ,js/modules/templify.js and css/enable-flyout.css from the repo and put them in the appropriate directories in your project (all JS files must be in the same directory).
  3. Load the CSS in the head of your document:
    ☜ Scroll to read full source ☞
    <html> <head> ... <link rel="stylesheet" href="path-to/css/enable-flyout.css" > ... </head> <body> ... </body> </html>
  4. Load your scripts using the following code (NOTE: you must use <script type="module">):
    ☜ Scroll to read full source ☞
    <script type="module"> import enableFlyout from "path-to/enable-flyout.js" enableFlyout.init(); </script>

Using ES4

Just do the same as the ES6 method, except you should get the JavaScript files from the js/modules/es4 directory instead of the js/modules/:
☜ Scroll to read full source ☞
<script src="path-to/es4/enable-flyout.js"></script>