Building accessible components can be time-consuming and complex. In the great confusion of aria-labels, tab indices and focus styles Adobe’s React Spectrum Libraries provide a set of React Hooks that will make your life so much easier - that is, if you're willing to take the time to understand them properly first.
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à!
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.
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.
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:
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:
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
- Pass them in 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
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.
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:
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:
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.
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.
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.
Here's how everything comes together in the Dropdown 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:
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.
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.
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!
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.