Menu Button

A button that opens a menu within a flyout.

Examples

Screenshot of delivery options menu on desktop

Introduction

A menu button opens a menu inside of a flyout.

A menu is appropriate when requiring a partial page re-render without using a form or full page reload. For example: filtering and sorting of search results.

A menu is not appropriate for a full page reload. For that, please use links instead (see the Fake Menu pattern). The distinction between menu items and links is important! A menu item is a command that executes JavaScript, whereas a link is a command that navigates to a url.

If your menu must contain a mix of JavaScript behaviour and link behaviour, please use a list of buttons and links. Do no mix menu items with links.

TIP: Do not call a menu button a "dropdown"! The term "dropdown" is ambiguous and could be confused with a listbox, combobox select or any other kind of overlay that "drops down". If you must, call it a dropdown menu.

Working Examples

You can take a look at the menu pattern in action on our examples site.

You can take a look at a fully styled menu button in action on the eBay Skin site

You can get a quick idea of the required markup structure by viewing our bones project.

Terminology

widget: the pattern as a *whole*, comprising the parts listed below

button: expands or collapses the overlay

collapsed/expanded: state of overlay

overlay: contains menu

menu: a menu that contains commands

command: individual menu item, menu item checkbox or menu item radio commands

Best Practices

See menu best practices.

Interaction Design

This section provides interaction design for keyboard, screen reader & pointing devices.

Please also see related menu pattern for best practices of nested menu.

Keyboard

The button must be keyboard focusable.

SPACEBAR or ENTER key on button must expand the menu.

When menu is expanded, keyboard focus must go to the first item in the menu.

UP-ARROW and DOWN-ARROW keys must navigate keyboard focus through commands via a roving tabindex.

If focus is on a command, ENTER or SPACEBAR keys must activate that command.

ESC key must collapse menu and return focus to button.

Activating any menu item should collapse menu (typically after a very short delay/transition).

TAB key must move keyboard focus off widget, and onto next interactive element in the page.

When widget loses focus, menu should collapse.

Screen Reader

Button label must be announced (e.g. 'Options').

Button state must be announced (e.g. expanded or collapsed).

Pointer

Clicking any menu item should collapse menu (typically after a very short delay/transition).

Developer Guide

While it is technically feasible for a menu to fallback to a set of form controls (i.e. button, checkbox and radio) while in a non-JavaScript state. However, as mentioned, this defeats the true purpose of a menu (which is to run JavaScript). Therefore our menu will be dependent on JavaScript.

Content (HTML)

For our developer guide we will create a menu that filters search results. The menu will be opened via a button.

Button

First we add our button:

<div class="button-menu">
<button type="button">Search Options</button>
<!-- overlay will go here -->
</div>

Ungrouped Menu

The simplest kind of menu contains just regular menu items. You can think of the menuitem role as similar to a button (but it doesn't fire a click event).

<div class="menu-button">
<button aria-controls="menu-1" aria-expanded="false" aria-haspopup="true" type="button">Search Options</button>
<div class="menu" hidden>
<div id="menu-1" role="menu">
<div role="menuitem">Show Less Results</div>
<div role="menuitem">Show More Results</div>
</div>
</div>
</div>

Notice the addition of ARIA on the button. If you ever wondered what aria-haspop is for, well now is the time to use it! To clear up confusion over this attribute, ARIA 1.1 introduced new values such as menu, dialog and listbox.

Grouped Menu

Menus can also contain groups. For example: a group of menu items, a group of menu item radios, and a group of menu item checkboxes.

Each group must be separated with a separator tag (implicit role="separator").

NOTE: We have encountered issues when trying to use role="separator" on a list tag. It is for this reason that use div-based markup, rather than list-base markup, in the examples.

<div class="menu-button">
<button aria-controls="menu-1" aria-expanded="false" aria-haspopup="true" type="button">Search Options</button>
<div hidden>
<div id="menu-1" role="menu">
<div role="presentation">
<div role="menuitem">More Results</div>
<div role="menuitem">Less Results</div>
</div>
<hr />
<div role="presentation">
<div aria-checked="true" role="menuitemradio">Sort by Name</div>
<div aria-checked="false" role="menuitemradio">Sort by Price</div>
<div aria-checked="false" role="menuitemradio">Sort by Date</div>
</div>
<hr />
<div role="presentation">
<div aria-checked="true" role="menuitemcheckbox">Show Buy It Now</div>
<div aria-checked="true" role="menuitemcheckbox">Show Auction</div>
</div>
</div>
</div>
</div>

We use ARIA to set the role and state of each command.

NOTE: A div tag already has an implicit role of presentation, so why have we specified it again explicitly? When testing in various screen readers, I noticed more consistent behaviour with the role specified explicitly.

Checkpoint

At this point we have our server-side markup with ARIA states.

You could also choose to render the overlay markup on the client, or even lazy-load it when the button is clicked.

Presentation (CSS)

The goal of our presentation layer is to add the hooks for hiding and showing the overlay.

The guidance here is actually identical to the flyout pattern, but we repeat it again here in the context of a menu.

The overlay is always hidden by default. The overlay must be absolute or fixed position with z-index.

.menu-button__menu {
display: none;
position: absolute;
z-index: 1;
}

We can display the menu utilising the ARIA state of the button and the general sibling selector:

.menu-button__button[aria-expanded="true"] ~ .menu-button__menu {
display: block;
}

Or leverage a class if necessary (i.e. if your button is not an adjacent sibling):

.menu-button--expanded .menu-button__menu {
display: block;
}

The overlay will only be visible when the aria-expanded state is true (or if the expanded class is present, if you choose). It is the job of JavaScript to toggle this state.

Behaviour (JS)

The goals of our behaviour layer are to:

  1. state management: toggle the aria-expanded state of button on click (and optionally add a class to the widget root element)

  2. focus management: implement roving-tabindex keyboard navigation on menu items

Toggling State

CSS alone cannot change the value of an HTML attributes; we require JavaScript. Fortunately, the makeup-expander module can handle this behaviour for us in just a few lines of code.

const Expander = require('makeup-expander');
const widget = new Expander(widgetEl, {
contentSelector: '.menu-button__menu',
expandOnClick: true,
collapseOnClick: true,
collapseOnFocusOut: true,
expandedClass: 'menu-button--expanded',
hostSelector: '.menu-button__button',
focusManagement: 'focusable'
});

When the menu opens, focus should move to the first menu item. This is handled by the focusManagement option.

Keyboard Navigation

Here is where we deviate from the flyout pattern. A generic flyout pattern makes no assumption about the contents of the overlay. We expect to use TAB key to move focus through any focusable children of the overlay. A menu has a very specific kind of content - menu items - and these menu items are navigated with the UP-ARROW and DOWN-ARROW keys.

We have another JavaScript module that helps, the makeup-roving-tabindex module.

const RovingTabIndex = require('makeup-roving-tabindex');
this._rovingTabIndex = RovingTabIndex.createLinear(widgetEl, '[role^=menuitem]' , {
autoReset: 0
});

View the roving tabindex technique for further details.

Utilities

We have some JavaScript modules that may assist you with creation of an accessible menu button widget:

ARIA Reference

This section gives an overview of our use of ARIA, within the specific context of the menu button pattern.

role=menu

Informs assistive technology that this is a menu containing menuitems, menuitemradios or menuitemcheckboxes.

role=presentation

Informs assistive technology that the divs around groups of menu items are for presentation purposes only and should not be added to accessibility tree.

role=menuitem

Informs assistive technology that this menu command has button behaviour.

role=menuitemradio

Informs assistive technology that this menu command has radio button behaviour.

role=menuitemcheckbox

Informs assistive technology that this menu command has checkbox behaviour.

aria-haspopup

Informs assistive technology that the button controls a menu. The name of this property is slightly misleading in that it implies it can be used for any kind of popup. This is not the case! This property is only for use with Popup Menus (not dialogs, tooltips, or bubble help, for example).

aria-controls

Inform assistive technology of which menu this button controls.

aria-expanded

Informs assistive technology whether the popup menu is expanded or not. And yes, this state goes on the button, not the menu.

aria-checked

Informs assistive technology whether the menuitemradio or menuitemcheckbox is checked or not. Notice we do not use aria-selected.