Building a better menu with roles and ARIA state

Using roles and ARIA can take a menu from a list of fairly innocuous items to something that is semantically very clearly a menu.

One of the most commonly mentioned and memed HTML faux pas is using div when a better choice of element is button. Divs are free of semantics; they just serve as a nondescript box in which to put things, and the browser or screen reader doesn't know what that box represents. While adding a click event to a div works in that clicking it will perform the expected action, you lose all the semantics and accessibility extras that come for free just by using the correct HTML element.

Most developers know that when building a menu the most appropriate element is an unordered list (ul) with list items for each menu item (li). A menu is pretty much a list so this is mostly fine, but there is more that can be done to make the purpose of the list absolutely clear.

Why do semantics matter?

The dictionary defines semantic as: 'relating to meaning in language or logic.' While you could build a page using only div that looks good, there is no meaning in the structure that can be read by any technology that presents the page to people.

Blind or partially sighted people use screen readers to have websites read to them aloud. The screen reader interprets the meaning behind an element on a website from the HTML tag used and the role that tag has, and can then give context to each part of the page as it gets focus. A list composed from div might look like a list visually, but a screen reader can't tell a user that they're interacting with a list.

Screen readers are just one example. Browsers have reading modes, and search engines rank pages with good semantics better than those without as they can better understand what's being crawled.

Implied roles

All HTML elements have a role assigned by default (though div and span have a role of 'null'). The button element has the role of 'button', ul is a 'list', and elements like main and header have a role of 'landmark'. In the majority of cases the right HTML element will provide the right roles, but HTML provides a way to override.

Explicit roles

div has an implied role of 'null' because there is no semantic meaning. If for some reason you needed to build a list of items using div and not ul/li you can explicitly define the role of each item as listitem and a parent as list:

<div role="list">
  <div role="listitem">Arial</div>
  <div role="listitem">Helvetica</div>
</div>

This is not something you should do unless it's absolutely unavoidable. Implied roles are always better than explicit.

Role in unit tests

In React Testing Library selecting by role is encouraged when getting elements rendered in a test.

Using getByText and getByTestId are based on some arbitrary characteristic of the element that has nothing to do with what it actually is. getByRole ensures the semantics of your HTML count; if you change the element to something with an inappropriate role the test will fail. This can take your tests from being focussed only on function to being concerned with how the component is built and used.

ARIA states

aria- attributes can be added to HTML elements to define a state. You might have a button that performs an action and whose appearance changes once it's been clicked to indicate that the action has happened. Just a visual change to the button isn't enough to set that change of state in the markup.

There are many ARIA states. The one most appropriate to this scenario is aria-pressed:

<button type="button" aria-pressed="true">Download fonts</button>

This isn't interchangeable with in-app state management, but you should set the attribute value at the same time as setting state to keep things synchronised.

Building the menu

With a combination of explicit roles and aria attributes we can make a menu that is better understood by browsers and screen readers.

Let's look at the entire menu then break it down:

<ul role="menubar">
  <li role="none">
    <a href="/arial" class="menu-item" role="menuitem">Arial</a>
  </li>
  <li role="none">
    <a href="/helvetica" class="menu-item" role="menuitem" aria-current="page">Helvetica</a>
  </li>
</ul>
  • Menus are usually a list of links, but here we're explicitly setting the role of the unordered list to 'menubar'.
  • Like any other list we use li for all items. As this is really only a container for the link itself we add a role of 'none' to remove any semantics.
  • The link itself is an a element with the explicit role of 'menuitem'.
  • State which menuitem is currently active with the aria-current attribute. In this case it's given the value 'page' but could be set to another value more appropriate to what's being represented, like 'step', 'location' or 'date'.

Styling

This article isn't about styling, but it's important to note that using ARIA attributes for selectors can help enforce the meaning and importance of the state they represent:

.menu-item {
  color: var(--type);
}

.menu-item[aria-current='page'] {
  color: var(--accent);
}

Colour isn't always the best method for making something look distinct from what's around it. Some other marker that's visible regardless of colour blindness or contrast can help keep things extra accessible.

Inspecting roles in the browser

The accessibility inspector in Firefox provides a view of the page structure separate from the DOM. Rather than a tree of HTML tags the page is described by nested roles. Any accessibility issues (expected page structure, click events on an element with the wrong role, etc.) are highlighted so it's a great tool for quickly auditing before releasing.

More posts

Previous

Marking external links with CSS

Next

Building a share button custom element