How to build an Accessible Dropdown using React Aria

by Nadia Posch

March 2022

Let me share some tips and tricks we learned using React Aria and React Stately in one of our recent projects. Also, I’ll walk you through how we built an accessible Dropdown where menu items can be passed in as a prop. Checkout the GitHub link at the end for a working example.

When to use React Aria, and Why

If you’re looking for a minimum effort solution to build accessible components, there's probably an existing component library out there that suits your needs. However, if your goal is to build your own custom component library, flexibility is key. And this is where React Aria comes into play.

React Aria leaves the styling and rendering of your components totally up to you. The only thing it provides is accessible behaviour in the form of DOM props.

The principle is easy: you import a hook, pass in some configuration props and a reference element. The hook returns a set of DOM props that can be spread on the element that should become accessible. And voilà!

useButton code example from the React Aria documentation

Sounds like a piece of cake? It is, at least for simple elements such as buttons or checkboxes. As soon as the components become more complex, so does the use of the hooks.

Drawbacks

Recently, we developed a custom component library together with one of our customers. The decision to support accessibility and use the React Spectrum Libraries was made only after some of the components had already been built. Introducing the hooks into the already finished components turned out to be quite challenging.

Destructuring

One of the drawbacks we encountered is the lack of destructuring. In the code examples from the docs, props are mostly passed in or spread out without specifying which ones are actually relevant. This makes it hard to understand how the hooks work and what kind of configuration props can be passed in.

In the Button component above, it’s impossible to know which configuration props are set without checking first which props are passed into the component. Instead, if the example were destructured in the following way, it becomes much more understandable:

Destructured version of the useButton code example

Typing

Another drawback is the typing. Although React Aria supports TypeScript, some types are inexplicit or more complicated than necessary.

One example is the Selection type in the onSelectionChange hook that’s used in multi-select lists. In the case where all options are selected, instead of passing in a set of all keys there’s a separate “all” string literal for this specific use case. Now, if you’d like to provide your own implementation of onSelectionChange, you need to implement an additional check as well as a getAllKeys method:

Code example for a custom onSelectionChange method

So, if you’re planning to use React Aria in one of your projects, I’d recommend you look through the code examples in the documentation beforehand in order to get a feeling for how the hooks work. Then you can start structuring your components accordingly. This leads me to the biggest obstacle we faced when introducing the hooks.

Children vs. custom props

One of the major differences in how we built our components and how React Aria hooks work is the use of children instead of custom props. Let’s take a Dropdown as an example. There are two ways to pass in the menu items:

Pass them in as an items prop

Dropdown example that expects items as a custom prop

Pass them in as children

Dropdown example that expects items as children

React Stately's useSelectState hook that’s used to control the internal Dropdown state expects the items as children. If you prefer to pass in the items as a prop however — like we did in our Dropdown — you have to find a workaround.

In the following section, I’d like to walk you through how our accessible Dropdown works in detail. Hopefully, it’ll give you a better understanding for how to work with React Aria and React Stately without having to dive as deep into the documentations as we did. The working example you find at the end is built using NextJS and Tailwind CSS. For the sake of simplicity, class names are left out of the code examples.

How to build an Accessible Dropdown using React Aria and React Stately

Screenshot of the accessible Dropdown that can be found on GitHub

The hooks we needed in order to make our Dropdown accessible were the useSelectState hook from @react-stately and the useSelect hook from @react-aria, as stated in the useSelect documentation:

useSelect requires knowledge of the options in the select in order to handle keyboard navigation and other interactions.

[…] useSelectState manages the state necessary for multiple selection and exposes a SelectionManager, which makes use of the collection to provide an interface to update the selection state. It also holds state to track if the popup is open.

Necessary props

We decided to pass in the menu items as a custom menuBlocks prop and not as children. Additionally, we needed an onChange method, the activeItemId of the currently selected item and an optional ariaLabel:

Type object for Dropdown props

Our design dictated that the menu items had to be grouped into sections separated by a divider. Each section had to have an id and an optional ariaLabel.  Here’s how the menuBlocks object looks that was used in the screenshot above:

Example menuBlocks object with two sections each containing two items

Mapping items to children

Since the useSelectState and useSelect hooks expect menu items as children, we had to map our menuBlocks array to an object with children. Also, we set the ariaLabel for the Dropdown and the sections.

It’s critical that you import the Section and Item components from @react-stately, otherwise they’re not compatible with the inner workings of the hooks. The content of the Item components is irrelevant though, because we won't render them in the same way as the docs propose.

Helper method to map menuBlocks to an object with aria-label and children

Now, we were all set to pass our propsWithChildren object to useSelectState. In addition, we set the currently activeItemId as well as the onChange method that's passed into the Dropdown in order to be able to track the state from outside the component.

Implementation of useSelectState with specific configuration props

Trigger, Overlay and Dropdown components

The trigger and overlay have to be referenced and made accessible with separate hooks: useButton for the trigger and useOverlay for the menu. The opening state of the overlay can be connected to the Dropdown state via the isOpen prop.

Concerning the FocusScope and DismissButton the documentation explains:

In addition, a <FocusScope> is used to automatically restore focus to the trigger when the popup closes. A hidden <DismissButton> is added at the start and end of the popup to allow screen reader users to dismiss the popup.

Code example for the Overlay component

Here's how everything comes together in the Dropdown component:

Code example for the Dropdown component

SelectMenu component

The SelectMenu component represents the ListBox that’s used in the code example of the useSelect documentation. And here comes the second reason I’d recommend you start thinking about how to structure your components before introducing the hooks: instead of mapping over an items array, the items are generated by mapping over the internal collection that’s passed in via the state:

Options are generated by mapping over the internal state collection

For us this meant that we weren’t able to access each item as we normally would when mapping over the original items array. The only prop we could access via the state was the item key (id) that was originally set in the mapToAriaProps method.

The solution we came up with was to flatMap all items from the menuBlocks, create a keyItemRecord and access the original item via the key of the childNode which corresponds to the item id. The item could then be passed to the MenuItem component as a prop.

Code example for the SelectMenu component

MenuItem component

The MenuItem component represents the Option that’s used in the ListBox and is the last puzzle piece that’s needed for the Dropdown. Thanks to the keyItemRecord we introduced before, we could now access all necessary information to render each menu item.

Code example for the MenuItem component

GitHub Repository

You can find a working example of the Dropdown on GitHub. Feel free to use and adapt it to suit your needs. Also, we appreciate your feedback!

Conclusion

React Aria and React Stately are powerful and flexible tools to create accessible components. Depending on the complexity of the components, more insight into the workings of the hooks is needed than just browsing the code examples.

For us, as with every foray into new territories, the initial uncertainty slowly gave way to a steep learning curve. Looking back, it would have been easier to rebuild our Dropdown from scratch and adapt the code structure to the React Aria hooks. However, the need to implement the hooks in our existing code gave us the opportunity to get a deep understanding of how the hooks work.

The key takeaway for me was that thinking about how to structure your components beforehand can save you a substantial amount of time. With that in mind, I would recommend React Aria and React Stately to anyone who plans to build their own custom component library. I hope that some of our learnings will ease your journey if you do decide to use the React Spectrum Libraries in your own projects.

Previous post
Back to overview
Next post