About The Author

Heydon Pickering ( @heydonworks ) has worked with The Paciello Group, The BBC, Smashing Magazine, and Bulb Energy as a designer, engineer, writer, editor, and … More about Heydon ↬

Email Newsletter

Weekly tips on front-end & UX .
Trusted by 200,000+ folks.

There are lots of different types of menu on the web. Creating inclusive experiences is a question of using the right menu patterns in the right places, with the right markup and behavior.

Editor’s Note : This article originally appeared on Inclusive Components . If you’d like to know more about similar inclusive component articles, follow @inclusicomps on Twitter or subscribe to the RSS feed . By supporting inclusive-components.design on Patreon, you can help to make it the most comprehensive database of robust interface components available.

Classification is hard. Take crabs, for example. Hermit crabs, porcelain crabs, and horseshoe crabs are not — taxonomically speaking — true crabs. But that doesn’t stop us using the "crab" suffix. It gets more confusing when, over time and thanks to a process called carcinisation , untrue crabs evolve to resemble true crabs more closely. This is the case with king crabs, which are believed to have been hermit crabs in the past. Imagine the size of their shells!

In design, we often make the same mistake of giving different things the same name. They appear similar, but appearances can be deceptive. This can have an unfortunate effect on the clarity of your component library. In terms of inclusion, it may also lead you to repurpose a semantically and behaviorally inappropriate component. Users will expect one thing and get another.

The term "dropdown" names a classic example. Lots of things "drop down" in interfaces, including the set of s from a element

If you’ve been involved in responsive design from the beginning, you may remember a pattern whereby navigation was condensed into a elements are menus of sorts, with similar semantics to the button-triggered menu we shall soon be constructing.

However, just as with the checkbox toggle button, we’re using an element associated with entering input, not simply making a choice. This is likely to cause confusion for many users — especially since this pattern uses JavaScript to make the selected behave like a link. The unexpected change of context this elicits is considered a failure according to WCAG’s 3.2.2 On Input (Level A) criterion.

True Menus

Now that we’ve had the discussion about false menus and quasi-menus, the time has arrived to create a true menu, as opened and closed by a true menu button. From here on in I will refer to the button and menu together as simply a “menu button”.

But in what respects will our menu button be true? Well, it’ll be a menu component intended for choosing options in the subject application, which implements all the expected semantics and corresponding behaviors to be considered conventional for such a tool.

As mentioned already, these conventions come from desktop application design. ARIA attribution and JavaScript governed focus management are needed to imitate them fully. Part of the purpose of ARIA is to help web developers create rich web experiences without breaking with usability conventions forged in the native world.

In this example, we’ll imagine our application is some sort of game or quiz. Our menu button will let the user choose a difficulty level. With all the semantics in place, the menu looks like this:



  • The aria-haspopup property simply indicates that the button secretes a menu. It acts as warning that, when pressed, the user will be moved to the “popup” menu (we’ll cover focus behavior shortly). Its value does not change — it remains as true at all times.
  • The inside the button contains the unicode point for a black down-pointing small triangle. This convention indicates visually what aria-haspopup does non-visually — that pressing the button will reveal something below it. The aria-hidden="true" attribution prevents screen readers from announcing “down pointing triangle” or similar. Thanks to aria-haspopup , it’s not needed in the non-visual context.
  • The aria-haspopup property is complemented by aria-expanded . This tells the user whether the menu is currently in an open (expanded) or closed (collapsed) state by toggling between true and false values.
  • The menu itself takes the (aptly named) menu role. It takes descendants with the menuitem role. They do not need to be direct children of the menu element, but they are in this case — for simplicity.

Keyboard And Focus Behavior

When it comes to making interactive controls keyboard accessible, the best thing you can do is use the right elements. Because we’re using

The open method

As part of a sound API design, we can construct methods for handling the various events.

For example, the open method needs to switch the aria-expanded value to “true”, change the menu’s hidden property to false , and focus the first menuitem in the menu that isn’t disabled:

		MenuButton.prototype.open = function () { this.button.setAttribute('aria-expanded', true); this.menu.hidden = false; this.menu.querySelector(':not(\[disabled])').focus(); return this; }

We can execute this method where the user presses the down key on a focused menu button instance:

		this.button.addEventListener('keydown', function (e) { if (e.keyCode === 40) { this.open(); } }.bind(this));

In addition, a developer using this script will now be able to open the menu programmatically:

		exampleMenuButton = new MenuButton(document.querySelector('\[aria-haspopup]')); exampleMenuButton.open();

Sidenote: The checkbox hack

As much as possible, it’s better not to use JavaScript unless you need to. Involving a third technology on top of HTML and CSS is necessarily an increase in systemic complexity and fragility. However, not all components can be satisfactorily built without JavaScript in the mix.

In the case of menu buttons, an enthusiasm for making them “work without JavaScript” has led to something called the checkbox hack. This is where the checked (or unchecked) state of a hidden checkbox is used to toggle the visibility of a menu element using CSS.

		/* menu closed */ [type="checkbox"] + [role="menu"] { display: none; } /* menu open */ [type="checkbox"]:checked + [role="menu"] { display: block; }

To screen reader users, the checkbox role and checked state are nonsensical in this context. This can be partly overcome by adding role="button" to the checkbox.


Unfortunately, this suppresses the implicit checked state communication, depriving us of JavaScript-free state feedback (poor though it would have been as “checked” in this context).

But it is possible to spoof aria-expanded . We just need to supply our label with two spans as below.


These are both visually hidden using the visually-hidden class , but — depending on which state we’re in — only one is hidden to screen readers as well. That is, only one has display: none , and this is determined by the extant (but not communicated) checked state:

		/* class to hide spans visually */ .vh { position: absolute !important; clip: rect(1px, 1px, 1px, 1px); padding: 0 !important; border: 0 !important; height: 1px !important; width: 1px !important; overflow: hidden; } /* reveal the correct state wording to screen readers based on state */ [type="checkbox"]:checked + label .expanded-text { display: inline; } [type="checkbox"]:checked + label .collapsed-text { display: none; } [type="checkbox"]:not(:checked) + label .expanded-text { display: none; } [type="checkbox"]:not(:checked) + label .collapsed-text { display: inline; }

This is clever and all, but our menu button is still incomplete since the expected focus behaviors we’ve been discussing simply cannot be implemented without JavaScript.

These behaviors are conventional and expected, making the button more usable. However, if you really need to implement a menu button without JavaScript, this is about as close as you can get. Considering the cut-down navigation menu button I covered previously offers menu content that is not JavaScript dependent itself (i.e. links), this approach may be a suitable option.

For fun, here’s a codePen implementing a JavaScript-free navigation menu button .

See the Pen Navigation menu button example no JS by Heydon ( @heydon ) on CodePen .

( Note: Only Space opens the menu.)

The “choose” event

Executing some methods should emit events so that we can set up listeners. For example, we can emit a choose event when a user clicks a menu item. We can set this up using CustomEvent , which lets us pass an argument to the event’s detail property. In this case, the argument (“choice”) would be the chosen menu item’s DOM node.

		MenuButton.prototype.choose = function (choice) { // Define the 'choose' event var chooseEvent = new CustomEvent('choose', { detail: { choice: choice } }); // Dispatch the event this.button.dispatchEvent(chooseEvent); return this; }

There are all sorts of things we can do with this mechanism. Perhaps we have a live region set up with an id of menuFeedback :


Now we can set up a listener and populate the live region with the information secreted inside the event:

		exampleMenuButton.addEventListener('choose', function (e) { // Get the node's text content (label) var choiceLabel = e.details.choice.textContent; // Get the live region node var liveRegion = document.getElementById('menuFeedback'); // Populate the live region liveRegion.textContent = 'Your difficulty level is ${choiceLabel}'; });
When a user chooses an option, the menu closes and focus is returned to the menu button. It’s important users are returned to the triggering element after the menu is closed.
When a user chooses an option, the menu closes and focus is returned to the menu button. It’s important users are returned to the triggering element after the menu is closed. ( Large preview )

When a menu item is selected, the screen reader user will hear, “You chose [menu item’s label]” . A live region (defined here with the role=“alert” attribution) announces its content in screen readers whenever that content changes. The live region isn’t mandatory, but it is an example of what might happen in the interface as a response to the user making a menu choice.

Persisting Choices

Not all menu items are for choosing persistent settings. Many just act like standard buttons which make something in the interface happen when pressed. However, in the case of our difficulty menu button, we’d like to indicate which is the current difficulty setting — the one chosen last.

The aria-checked="true" attribute works for items that, instead of menuitem , take the menuitemradio role. The enhanced markup, with the second item checked ( set ) looks like this:


Native menus on many platforms indicate chosen items using check marks. We can do that with no trouble using a little extra CSS:

		[role="menuitem"] [aria-checked="true"]::before { content: '\2713\0020'; }

While traversing the menu with a screen reader running, focusing this checked item will prompt an announcement like “check mark, Medium menu item, checked” .

The behavior on opening a menu with a checked menuitemradio differs slightly. Instead of focusing the first (enabled) item in the menu, the checked item is focused instead.

The menu button starts with the menu unopened. On opening the second (Medium) difficulty setting is focused. It is prefixed with a check mark based on the aria-checked attribute's presence.
The menu button starts with the menu unopened. On opening the second (Medium) difficulty setting is focused. It is prefixed with a check mark based on the aria-checked attribute's presence. ( Large preview )

What’s the benefit of this behavior? The user (any user) is reminded of their previously selected option. In menus with numerous incremental options (for example, a set of zoom levels), people operating by keyboard are placed in the optimal position to make their adjustment.

Using The Menu Button With A Screen Reader

In this video, I’ll show you what it’s like to use the menu button with the Voiceover screen reader and Chrome. The example uses items with menuitemradio , aria-checked and the focus behavior discussed. Similar experiences can be expected across the gamut of popular screen reader software.

Inclusive Menu Button On Github

Kitty Giraudel and I have worked together on creating a menu button component with the API features I have described, and more. You have Hugo to thank for many of these features, since they were based on the work they did on a11y-dialog — an accessible modal dialog. It is available on Github and NPM.

		npm i inclusive-menu-button --save

In addition, Kitty has created a React version for your delectation.


  • Don’t use ARIA menu semantics in navigation menu systems.
  • On content heavy sites, don’t hide structure away in nested dropdown-powered navigation menus.
  • Use aria-expanded to indicate the open/closed state of a button-activated navigation menu.
  • Make sure said navigation menu is next in focus order after the button that opens/closes it.
  • Never sacrifice usability in the pursuit of JavaScript-free solutions. It’s vanity.
Smashing Editorial (il)