Building an Accessible Dropdown Menu
From keyboard navigation to focus management, the humble dropdown is more complex than it seems
As Pedro Duarte said in his video demonstrating the capabilities of Radix Primitives, building an accessible dropdown is much harder than it appears on the surface.
There are countless factors to consider, from keyboard navigation and focus management to aria-roles and screen reader support the Menu Button WAI-ARIA design pattern lists many design details to implement and test, as well warnings and common mistakes.
For example, the button
element that triggers the Dropdown
should include the following interactions while focus is on the button:
- Enter: opens the menu and places focus on the first menu item.
- Space: opens the menu and places focus on the first menu item.
- Down Arrow (optional): opens the menu and moves focus to the first menu item.
- Up Arrow (optional): opens the menu and moves focus to the last menu item.
Accessible Primitives
There are a number of open source libraries who's goal it is to make building accessible components easier. These components typically come unstyled, and offer a variety of different ways to compose and style them to fit into your design system.
- Radix Primitives: a set of primitives that are styled by default.
- Headless UI: from the team behind Tailwind CSS, a set of primitives that are styled by default. Comes in
React
andVue
flavors. - Reakit: Accessible, composable, customizable, tiny & fast.
- React Aria: A library of React Hooks that provides accessible UI primitives for your design system.
For Graham Media Group's design system we chose to use Headless UI's Menu (Dropdown) for it's simple and composable API. While this package is written and maintained by the Tailwind CSS team, it is not dependant on Tailwind for styling.
We use styled-components and a TypeScript variant of styled-system for styling, and gaining access to all of our defined style props was as easy as passing transforming a component into a primitive through the as
prop. The component will merge the prop interfaces of the primitive and the component, and the primitive will be styled with the component's props.
import { Menu, Transition } from "@headlessui/react";
import { Box } from "../Box";
import { Button } from "../Button";
function Dropdown({
children,
buttonText = "Open",
variant = "$primary",
size = "rg",
}) {
return (
<Menu>
{({ open }) => {
return (
<Box>
{/* By using the polymorphic `as` prop, we gain access to `size` and `variant` */}
<Menu.Button as={Button} size={size} variant={variant}>
{buttonText}
</Menu.Button>
<Transition show={open}>
<Menu.Items as={Box} sx={menuItemStyles} static>
{children}
</Menu.Items>
</Transition>
</Box>
);
}}
</Menu>
);
}
By composing the primitives from the @headlessui/react
package within our component library, we are able to distribute variant derived styles to the Menu.Button
, Menu.Items
and Menu.Item
components to allow for easy customization, and to allow for easy composition of in the consuming application.
In Action
The primary use-case of the dropdown menu is the <AccountMenu />
component, which lives in the site header and is only accessible to logged in users. This provides a quick way to interact with our third party comment provider, navigate to popular pages and account features such as logging out.