Last week I watched Pedro Duarte's excellent "So You Think You Can Build A Dropdown" talk at Next.js Conf. It inspired me to write up an accessible component of my own that I recently worked on β the menubar widget.
I have a real interest in accessibility, particularly in frontend web development. Of all the patterns that I've researched to date, the menubar was the most complex. Reach, Radix, and React Aria all provide flexible and accessible React components.
Yet, I struggled to find any library that provided a menubar component out of the box. Given the complexity and lack of material, I thought I'd share my discoveries with the community.
Introduction
This article will explain how I created an accessible 
Loading...
 component with React. The aim was to create a component that adhered to the WAI-ARIA 
design pattern for a menubar widget.
For brevity, the article will focus on a horizontal menubar with a single submenu. It also assumes you are comfortable with React hooks and the compound component pattern. I've included the  solution as a Code Sandbox link below.
Useful Links
The Menubar
We'll kick off with the requirements. The Mythical University has requested an accessible site navigation for their website.
To get started, we'll group a collection of hyperlinks in an unordered list. We'll also wrap the list in a navigation section.
The HTML might look something like this:
Loading...
At first glance, the markup looks comprehensive, but how accessible is it for those reliant on assistive technologies? Additionally, can the user navigate the menubar with the expected keyboard controls?
Although we have provided semantic HTML, the current iteration is not considered accessible. The markup is missing critical 
Loading...
 roles that give context to both the links and the widget itself. Poor keyboard support also means the user is only able to tab through the list of links.
Let's improve both of these areas.
We'll start by creating two functional components. One is a parent 
Loading...
 list, and the other is a child 
Loading...
 list item. Together we'll use these to compose a compound 
Loading...
 component.
The parent 
Loading...
 returns an unordered list element. Since it's the widget's root element, we'll assign it the 
Loading...
 role. The 
Loading...
 attribute allows assistive technology to determine the direction of the menu. Finally, let's include a custom 
Loading...
 attribute for targeting and styling later on.
Loading...
The second component is the 
Loading...
. It accepts a single node for its 
Loading...
 prop and returns the node wrapped in a list item element.
Assistive technology should only announce the child node. A list item element has the 
Loading...
 role by default. By overriding it to 
Loading...
, we completely remove it from the accessibility tree. We then assign the child node the 
Loading...
 role by 
cloning the element and shallow merging the prop.
Loading...
Finally, let's add a matching 
Loading...
 to the navigation element.
The current React markup will look something like this:
Loading...
Which will compile into the following HTML:
Loading...
So far we've improved the menubar for those using assistive technology, but what about those who are reliant on keyboard controls? For them to navigate the list of menu items, the 
Loading...
 component needs to be aware of each child 
Loading...
. We can achieve this by utilizing the React 
Loading...
 and 
Loading...
 hooks.
Let's start by creating a new 
Loading...
:
Loading...
The 
Loading...
 will store a 
Set of nested 
Loading...
 nodes within a parent 
Loading...
. We contain the 
Loading...
 in a mutable ref object created with the 
Loading...
 hook, and store the 
Loading...
 value in a variable.
This allows us to manipulate the 
Loading...
 contents without re-rendering the 
Loading...
. Next, we'll memoize an object with the 
Loading...
 hook and assign the 
Loading...
 as a property. Finally, we'll pass the object to the value attribute of the 
Loading...
.
Loading...
The 
Loading...
 should only ever be a child of a 
Loading...
 component. To enforce this, let's throw an error if the 
Loading...
 hook cannot find a 
Loading...
. This allows us to assert that 
Loading...
 exists below the following conditional statement:
Loading...
Let's create an object reference to the 
Loading...
 DOM node with the 
Loading...
 hook. Then let's use the 
Loading...
 hook to trigger a side-effect that adds the node to the 
Loading...
 Loading...
. We'll also return a cleanup function to remove it from the 
Loading...
 if the 
Loading...
 unmounts.
Loading...
Roving tab index
We now have a reference to each 
Loading...
 node. With them, we can apply the 
roving tab index pattern to manage focus within the component. To do that, the 
Loading...
 needs to keep track of the current and previously-focused 
Loading...
. We can do this by storing the indexes of the current and previous nodes in the 
Loading...
's component state.
The current index is a stateful value stored using the React 
Loading...
 hook. When the Menubar first mounts, the first 
Loading...
 child should have a tab index of 
Loading...
. Thus, we can assign 
Loading...
 as the default state for the current index.
We can use a custom hook to track the previous index. The hook accepts the current index as a function parameter. If the hook does not return a value, we can assume that one does not exist and fall back to 
Loading...
.
Loading...
To apply the roving tab index, the 
Loading...
 node must have a tab index of 
Loading...
. All other elements in the component's tab sequence should have a tab index of 
Loading...
. Whenever the user navigates from one menu item to another, the following should occur:
- The current node should blur and its tab index should set to Loading... 
- The next node's tab index is set to Loading... 
- The next node receives focus
Let's utilize the React 
Loading...
 hook for this. We'll pass the current and previous indexes as effect dependencies. Whenever either index changes, the effect will update all appropriate indexes. Note that we are applying the tab index attribute to the first child of the 
Loading...
, not the list item wrapper.
Loading...
We donβt have to add the tab index to each menu item, we can update the 
Loading...
 component to do that for us! We can assume that if the 
Loading...
 Loading...
 is empty, then the node is the first menu item in the sequence.
Let's add some component state to track whether the 
Loading...
 is the first node in the set. If it is, we can assign its tab index a value of 
Loading...
 β otherwise, we'll fall back to 
Loading...
.
Loading...
Keyboard controls
Next, we'll use the 
Loading...
's 
Loading...
 event to update the current index based on the user's keypress. There are five primary methods that a user can navigate through the menu items. They can:
- Return to the previous item
- Advance to the next
- Jump to the first
- Skip to the last
- Move to the next match
Let's encapsulate that logic into some helper methods that we can pass to the 
Loading...
 event.
Loading...
With the helper methods defined, we can assign them to the appropriate key codes. We'll check to see if the keypress matches any keys associated with movement; if it doesnβt, we'll default to the 
Loading...
 helper method.
Loading...
Notice that we are calling 
Loading...
 on most of the helper methods. This is to suppress any default browser behavior as the user interacts with the menubar. For example, by default, the 
Loading...
 key scrolls the user to the bottom of the page.
Let's say we did not prevent the default behavior; the scroll position would jump to the bottom of the page any time the user tried to skip to the final menu item!
We mustn't call 
Loading...
 on the default case. If we did, it would ignore any default browser behavior not captured by a switch case. This could lead to undesired behavior. An example would be if a menu item within the menubar had focus and the user pressed 
Loading...
 to refresh the page. If we called 
Loading...
 on the default case, it would ignore the refresh request. It would then pass the 
Loading...
 key to the 
Loading...
 helper method.
We now have a fully-accessible Menubar widget for a collection of navigation links! Each menu item provides rich contextual information to assistive technology. It also allows those reliant on keyboard support to navigate the list of links as they would expect.
The component API hasn't changed from the previous example...
Loading...
...yet the compiled HTML markup now includes tab indexes on the menu items.
Progress!
Loading...
The Submenu
The previous example is great for a single collection of links, but what if we replaced one of them with a dropdown that revealed a secondary set of navigation links?
Loading...
For this, we're going to need to create a second compound component β the 
Loading...
. It is composed of three functional components:
- The Loading... will hold shared logic and component state
- The Loading... will allow the user to expand the menu
- The Loading... will display the expanded menu items
The 
Loading...
 keeps track of menu items within the 
Loading...
. In turn, let's create a 
Loading...
 to keep track of menu items nested within a 
Loading...
.
Loading...
Let's start by defining the 
Loading...
 component. It'll share some similar behaviors and functionality to the 
Loading...
. Alongside the index tracking, it also needs to know if its menu has expanded. We could declare another state variable with 
Loading...
. Instead, it makes more sense to merge the logic into a reducer function.
The purpose of the 
Loading...
 parent component is to hold the compound component state. It is also responsible for distributing shared logic to its sub-components. We assign the logic to a memoized object, after which that object is then passed to the value attribute of a 
Loading...
.
Loading...
Now, let's define the helper methods for navigating the submenu's menu items. These are almost identical to the 
Loading...
 helpers. The key difference is they dispatch reducer actions instead of updating the component state directly.
Loading...
Some functional requirements need the subcomponents to have knowledge of their sibling. We can achieve this by defining ids and references for each subcomponent in the 
Loading...
. Note that we store the 
Loading...
 within a reference object. This is to prevent the 
Loading...
 function from regenerating the id on every render. Each subcomponent can now retrieve the values from the 
Loading...
 hook.
Loading...
Let's now manage focus within the 
Loading...
. We'll start by adding another side effect. This one will focus the first child of the current index if the tracked indexes do not match. Whenever we update the current index, we focus the first child of the new current node.
Loading...
Submenus do not follow the roving tab index pattern. Instead, the tab index of each menu item within a submenu will always be 
Loading...
. This requires a small change to the 
Loading...
 component. If a 
Loading...
 exists, we can assume the 
Loading...
 is inside a 
Loading...
 and apply 
Loading...
 to its tab index.
Loading...
Trigger
With the 
Loading...
 defined, let's create the 
Loading...
 component. We'll start by retrieving the 
Loading...
 and 
Loading...
 from the 
Loading...
. Since a button's default type is 
Loading...
, it's usually a good idea to override it to 
Loading...
.
Finally, the 
Loading...
 should only ever be a child of the 
Loading...
. Like before, let's throw an error if we use it outside of a 
Loading...
.
Loading...
Next, let's add the appropriate 
Loading...
 attributes. 
Loading...
 will inform assistive technology that the button controls a submenu. To go one step further, we can also add the 
Loading...
 attribute. This informs the screen reader of the exact submenu controlled by the 
Loading...
.
Let's also retrieve the 
Loading...
 and the 
Loading...
 state from the 
Loading...
. We'll assign the 
Loading...
 to 
Loading...
. Then, all that's left is to assign the 
Loading...
 state to the 
Loading...
 attribute. Assistive technology is now aware of the menu button controls, and whether they are open or closed.
Loading...
Now, let's add keyboard support to the 
Loading...
. The 
Loading...
 will be a sibling of the Menubar menu items. That means it should perform the same 
Loading...
 events as the Menubar links. It also requires some additional functionality. Alongside the menu item behavior, the Trigger should:
- Loading... : Open the submenu and focus the last item
- Loading... : Opens the submenu and focus the first item
- Loading... ,- Loading... : Open the submenu and focus to the first item
To do this, we'll retrieve some methods from the 
Loading...
 and assign them to the relevant 
Loading...
. Note that we only want to execute the 
Loading...
 method on unique events.
Doing so allows all other events to bubble up to the 
Loading...
. This is what prevents us from having to duplicate the menu item's 
Loading...
 events.
Loading...
Let's say a submenu is open when the user presses the 
Loading...
or 
Loading...
 key. The submenu should close and focus the previous or next 
Loading...
 menu item. If the root menu item is also a submenu, it should expand the menu but keep focus on the trigger.
The 
Loading...
 achieves this by checking to see if the event originated from a submenu menu item. This ensures that the menu does not expand when other 
Loading...
 methods focus the trigger.
Loading...
List
Now that we have a 
Loading...
, all we need to do is create a submenu 
Loading...
. Like the 
Loading...
, we'll throw an error if the 
Loading...
 component is not used within a 
Loading...
.
Let's also define some attributes. First, we'll apply the 
Loading...
 and retrieve the 
Loading...
 from the 
Loading...
. We'll retrieve 
Loading...
 from the context and assign it to the 
Loading...
 attribute. This will hide the List from the accessibility tree if the menu is not expanded.
Next, let's label the menu by assigning the 
Loading...
 to the 
Loading...
 attribute. Finally, we'll supply the menu's direction to assistive technology with the 
Loading...
 attribute.
Loading...
Now let's add some 
Loading...
 events specific to the 
Loading...
 component. We'll retrieve the appropriate helpers from the 
Loading...
. Again, we only want to stop propagation on events that we do not want to bubble up to the 
Loading...
's 
Loading...
 event.
Loading...
The 
Loading...
 component will work within a 
Loading...
 for the most part. We'll need to make a couple of changes to ensure that both the 
Loading...
 and 
Loading...
 can make use of the component.
The first change is to ensure that the correct 
Loading...
 Loading...
 receives the 
Loading...
 node. We can assert that a submenu is an ancestor element if the 
Loading...
 can retrieve a 
Loading...
. If it returns a false value, then the 
Loading...
 must belong to the Menubar.
Let's update the error to check for the 
Loading...
. The error should only throw if both contexts do not exist. A 
Loading...
 can now be a child of either a 
Loading...
 or a 
Loading...
.
Loading...
There is one final change that we need to make to the 
Loading...
 component. Let's revisit the structure of the 
Loading...
.
The 
Loading...
 currently clones its 
Loading...
 prop and appends extra props. In the example below, we can see that 
Loading...
's child is the 
Loading...
 component. The 
Loading...
 returns a context provider as its parent element. The provider returns nothing from render, and so the props are not attached to any DOM node.
Loading...
Instead, we would like to append the 
Loading...
's 
Loading...
 onto the submenu 
Loading...
. To do so, the 
Loading...
 component will need to check its 
Loading...
's type.
If the type is a node, then we clone it and append the props. If the type is a function, then we instead provide the props as an argument in the function signature.
This allows us the flexibility of choosing which element should receive the props and additionally retains the convenience of appending the props onto the child by default.
Loading...
That leaves us with this flexible React markup:
Loading...
...which compiles into this beautiful, accessible HTML:
Loading...
Now, all that's left is to add extra logic for mouse pointer events, nested submenus, and a full suite of unit tests!
Unfortunately, we'll consider these features out of scope for this article and they would warrant a follow-up post to cover. I've included all the extra logic and the unit tests in the Code Sandbox demo at the top of the page.
Special thanks to Jenna Smith for her invaluable contributions to the initial API design.