Prologue
A while ago, I decided to develop a fully accessible main navigation component in React after a fruitless search through third-party component libraries, npm packages and even GitHub repositories.
A complex component needs requirements around all aspects of the component, and this article begins the process of defining those requirements.
Note: This article is one of a series demonstrating building a React navigation component from scratch while considering accessibility through the process. This article addresses requirements gathering and architectural decisions to be considered before code is written.
This article is fairly long, and so a set of content links is provided.
Content Links
- Introduction
- Wishlist
- Gathering Requirements
- Development Requirements
- HTML Structure
- Handling List Exposure
- Next Steps
Introduction
This article is the second in a series in which I walk through the steps I've taken to create an accessible navigation component with React. In my previous article, I covered the basic requirements for evaluating third-party components while seeking an accessible navigation component.
When I didn't find any component that met even the minimal requirements, I decided to build my own, which means gathering even more requirements.
Whether you call them acceptance criteria or requirements, it means the same thing—instructions detailing what is required for a ticket to be considered complete. Acceptance criteria are a contract between those who assign the tickets, those who code and those who test.
I've seen very few tickets over the course of my career that offer detailed user stories. Many lack adequate acceptance criteria as well. And that might be well and good if developers and testers were fully conversant with accessibility and the requirements for any specific component.
That isn't our reality, and depending on an LLM to add in accessibility basically sets you up for failure. Detailed requirements require detailed knowledge. If your business analysts lack the knowledge, depending on your developers to bring it probably won't help.
I've seen more than my share of tickets where scrum masters have decided that marking "Must conform to WCAG" as the definition of done is adequate, with no further consideration of what that entails. Even worse is having that phrase show up as an acceptance criterion. What does a phrase like that mean for the development and testing of a specific component?
I'm not saying that every requirement for a complex widget whose development will span across sprints needs to be in place before the first ticket is written. But the most important and consequential requirements, the ones that will drive how a component is architected, have to be there before the first ticket is scheduled and ready to be picked up.
If I'm going to take the requirements I used to analyze third-party packages and expand them to create a universally accessible component, I have to consider exactly what I want from the navigation component of my dreams.
Wishlist
- The ability for the component to display as either controlled (opened and closed from another component) or uncontrolled, so that I can use the same component when rendering for desktop and mobile.
- The ability for the top row of the component to be displayed horizontally (desktop) or vertically (mobile).
- The ability to render a JavaScript object into a properly formed navigation component.
- The ability to render links and buttons, manage sublists within the same list, and support nesting down to at least two levels of navigation.
Gathering Requirements
Requirements for users of screen and pointer are fairly easy to come up with, and without specific acceptance criteria, would most likely be what is coded for a ticket with a user story of "As a user, I want to be able to access the main navigation and move to another page on the website."
But I want more. I want to be sure my navigation component is perceivable through both screen and screen reader and operable through pointers, keyboard and voice.
Wearing my business analyst hat (🖊), the first question that comes to mind is: Where can I find the starting requirements for a main navigation component?
APG
I'll start by returning to the disclosure navigation menu pattern available in the Aria Pattern Guide, also known as the APG.
The APG isn't as consistent as I would like; requirements have to be extrapolated rather than being considered canon. Accessibility features and keyboard support especially need to be considered holistically, not as blueprints.
- The main list is wrapped in a < nav / > element.
- Open dropdowns should close when Escape is pressed, or focus moves out of the navigation region.
- There should be a visual indicator of the show/hide state.
- Navigating through a horizontal navigation component should prevent the default page scroll behavior.
- Navigating with arrow keys does not replace tabbing through the buttons and links.
- Keyboard behavior
- See Keyboard support
Keyboard behavior, as written, is inconsistent and must be extrapolated. When the reference is from link to link or from button to button, it assumes the navigation must conform to the sample code displayed. Not every navigation will be this rigid; buttons opening sublists may, in fact, be somewhere within another sublist, not just on the top row. The requirement to change the current page when pressing the Enter Key or to display a visual indicator of the show/hide state is also a function of the example and not a specific requirement. However, it does highlight the need for a high-contrast theme (which is not included in this demonstration).
The Role, Property, State and TabIndex section is specific to the aria attributes necessary.
- aria-controls - requires each button controlling a list to be linked to the list via an ID on the list.
- aria-expanded (true/false) indicates the state of the list's visibility on the button.
- aria-current (page) indicates a link associated with the current page.
Deque Accessibility Checklist
There's another list to refer to: Deque's accessibility checklist. I find the PDF useful because some requirements are scattered across topics, and I can search it.
The requirements I'm looking for are under site navigation, where three topics are listed: Consistency, Multiple Ways and Navigation Lists.
Consistency is achieved by developing a component that works in both desktop/laptop and mobile environments and delivers the same navigation across pages. The development of the component that can be activated in the header achieves this goal.
Multiple Ways requires that multiple ways to find other pages on the site be available. Developing a navigation component is one way.
These aren't requirements for this component, though they are important at the site-wide level. The requirements I need are in the last topic.
Navigation Lists have two basic requirements: Markup, which requires the navigation list to be contained within a <nav/> element; and a user must always have an indication of which page in the list is the page the user is currently viewing. The requirement specifies that indicators must be set up for both screen and screen reader users.
Development Requirements
Back when waterfall development was a thing, all requirements had to be gathered before any programming began. Agile, on the other hand, either crams requirements into a Confluence or SharePoint space, rarely acknowledges them on tickets, or skips detailing front-end requirements and relies on developers' expertise to build a component properly, usually based on a design document.
Acceptance criteria are the requirements that direct developers to the outcomes required and provide a clear path for testing them. In my experience, across government agencies and private companies, it's been rare to see any acceptance criteria that spell out accessibility requirements beyond the vaguely worded "must meet WCAG 2.x".
Why aren't accessibility requirements spelled out? My guess is that no criteria are given because it's rare for someone to understand accessibility and craft the specific requirements around it. It sets up a fail spiral, or at least creates a lot of hard-to-fix tech debt.
If a component will be developed over a series of sprints, I don't think all specific requirements need to be finalized before the first ticket is assigned; however, all accessibility requirements for a particular ticket should be fully defined before work begins.
Between the wishlist, evaluation requirements and the information I've extrapolated, I can begin assembling a list of requirements/acceptance criteria and specify which ticket each is associated with. Below is a list of headings indicating possible tickets to write, along with some of the requirements.
You'll note that the earliest ticket, Structure and Transformation, has the most requirements, while subsequent tickets are less precise, and some have no requirements yet, only the knowledge that they will be necessary. That's Agile: the requirements/acceptance criteria only need to be in place before ticket grooming.
A list of the initial development requirements may be found on the Requirements Matrix and are also given below.
You may skip to the requirements discussion if you are viewing the matrix.
Structure and Transformation
- The Navigation Component should be contained in a landmark region. The navigation should be presented as a nested unordered list within the < nav/> landmark region.
- Lists should present with a role of "list", list Items should present with a role of "listitem".
- The navigation component should be able to be presented as horizontal (desktop - default) or vertical (mobile presentation).
- When navigation is displayed as uncontrolled in a horizontal layout (the default behavior), the first row of focusable elements should display as open by default.
- Buttons are associated with a sublist and indicate if the sublist is open or closed.
- A visual indicator is used to represent the state of the sublist's expanded status.
- A sublist may be toggled to open or close when the button associated with it is pressed.
- The state of a list's visible or hidden state should be easily perceived by any user
- A link should inform users if it corresponds to the current page for both screen and screen readers.
- A JavaScript object containing a properly formed object should be able to be transformed into properly formed ListItems, Buttons and Subnavigation lists.
The structure and transformation requirements detail the main HTML structure and state changes when a sublist is closed or opened. After the requirements in this section are completed, a skeleton and pointer usable navigation component should be fully realized. Enough to be able to support concurrent design and styling work, and as a base for adding in all the rest of the requirements to enable full accessibility, including keyboard handling.
Single List Keyboard Handling
- A Single List should implement Keyboard navigation with the following Keys: Home, End and the arrow keys.
- Within a single list, the arrow keys should shift focus to the next (right arrow) or previous (left arrow) focusable element within the current list.
- Within a single list, the Home key should shift focus to the first element on the current list.
- Within a single list, the End Key should shift focus to the last element on the current list.
Keyboard handling begins with handling focus for just a single list. Nothing more.
Data Handling Between Components
- No requirements detailed yet
While not much is known yet, it's clear that finding list items within list components will require a link or button in a list to access information, not only within the elements in the list it resides in, but also in other nested lists associated with focusable elements.
Keyboarding Between Components Up/Down
- The Down Key should shift focus to the next element within a list and down between open sublists.
- The Up Key should shift focus to the previous element within a list and up between open sublists.
Another keyboard ticket, this time for allowing focus to shift between list components, dependent on data handling.
Keyboarding Between Components Tab/Shift+Tab
- The Shift+Tab Key should shift focus to the previous element within a list and up between open sublists.
- The Tab Key should shift focus to the next element within a list and down between open sublists.
Tab and Shift+Tab should always move the focus to the next item in the DOM.
Data Handling For Closings
- No requirements detailed yet
What happens when a list is closed? How does it handle other open lists? While not enough is known to begin creating requirements at this stage, it's possible some data will need to be passed between list items residing in different components. The requirements in the next section: Closings, Entries and Exits should help drive the requirements for data handling.
Closings, Entries and Exits
- The Tab and Shift Tab Keys should allow exiting the component at both the first and last focusable elements.
- When Escape is pressed anywhere in an uncontrolled navigation component, all open sublists close and focus shifts to the topmost parent.
- When focus shifts outside the navigation component, all open sublists close.
A complete component allows for movement into and out of itself using the Tab Keys. Requirements are also necessary to determine what happens with any open sublists when a component is exited.
Controlled Navigation
- When the controlling element is activated, and the navigation component is closed, the navigation component should open and set focus on the first child in the first row.
Rather than create two separate components for desktop and mobile, the goal is to combine them into a single component. There will be differences to handle when a button controls the navigation component and displays the top row vertically rather than horizontally. Any differences in display and keyboarding will need requirements moving forward.
Requirements Discussion
If it wasn't obvious before, this component won't be a simple one-sprint-and-done. It's going to take time, thought and resources. It's okay at this stage to know something needs to be done, even if you don't know the exact specifications. The component will need to manage data, but what the data is and how it should be managed still needs to be considered.
Grouping requirements allows similar work to be handled in a single ticket and layers accessibility into development through progressive enhancement, which is how these articles and the code will be structured.
Speaking of structure.
HTML Structure
When possible, a valid HTML structure should be decided upon before any code or design work is begun. Knowing what the rendered code should look like and working towards a skeleton component that provides the structure first means design and development can proceed concurrently, ensuring styling and operability can be easily integrated.
When it comes to development, operability has to come first. It doesn't matter how much time and effort have gone into making something look good if the underlying structure supporting it makes it harder to operate or style. A developer should be tasked with making sure the component works correctly before being required to make it look a certain way.
Conversely, those tasked with creating a design, especially if they're also familiar with CSS, can receive a skeleton component with basic functionality or just the HTML structure requirements for a Figma or other design system output, and ensure the design works with the structure.
In this case, the structure output should resemble the HTML below.
<nav>
<ul>
<li><a href="#" id="item-one">Item One</a></li>
<li><a href="#" id="item-two">Item Two</a></li>
<li>
<button id="item-three">Item Three</button>
<ul id="subnav-1">
<li><a href="#" id="item-four">Item Four</a></li>
<li><a href="#" id="item-five">Item Five</a></li>
<li>
<button id="item-six">Item Six</button>
<ul>
<li><a href="#" id="item-seven">Item Seven</a></li>
<li><a href="#" id="item-eight">Item Eight</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</nav>
The agreed-upon HTML structure must be considered valid; this can be verified by running the code through an HTML validator. I use the W3C Nu HTML Checker, which can test code from a URL or a file upload, as well as rendered code copied from a browser's dev tools.

Screenshot showing validated HTML structure
Handling List Exposure
Currently, the requirements specify opening and closing subnavigation lists but don't specify how to do so, leaving it to the developer assigned to the ticket. How the list is rendered makes a big difference to accessibility, so the determination and acceptance criteria for how sublists are rendered should be written before the first ticket is in progress.
There are three ways to disclose previously hidden subnavigation.
- Choose only to render the subnavigation when it is open
{open && (<ul>…</ul>}. - Choose to hide closed navigation lists using
display: none. - Choose to hide the list from screen view, while still exposing it to screen readers using a screen reader-only class (.srOnly).
To adequately assess these choices, an informed understanding of how users interact with screen readers is necessary. Every screen reader provides multiple options for finding specific element types that are rendered on a page.
- Navigate to a specific landmark role or heading and then use the tab key to move through the page from that particular point.
- Pull up a list of similar structural elements and allow the user to move through the list regardless of where those elements are located on a page. This is called the elements list in NVDA and Jaws, while VoiceOver calls it a rotor.
Most disclosure patterns in React tend toward option 1: render the list only when it is visible. The second option, using display: none, effectively hides the closed sublist from both screen and screen readers, and is the behavior demonstrated in the example shown for the APG Navigation Disclosure pattern. Neither option exposes the full list of navigation links within the elements list/rotor and forces screen reader users to navigate the list in the same way a screen user does, by navigating to sublists and finally finding the link they want.
Within a screen reader, the elements list/rotor exposes all interactive and structural elements, such as landmarks and headings, available on a page when it loads or re-renders. This includes all the other links contained in the header and footer. Since screen reader users regularly use the elements list/rotor to navigate a site, is it equitable to only provide links if they are visible on the screen?
Option 3 makes the full list of navigation links available in the elements list/rotor as soon as the page is fully loaded. I would argue that the main navigation links should be fully accessible to screen readers at all times, enabling a screen reader user to seamlessly discover their next navigation target regardless of how they choose to find it.
A class that hides items from the screen while still keeping them available in the DOM should be globally available. The class is fairly common, and variations can be found through web searches. The one I use is:
.srOnly {
.srOnly {
border: 0;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden !important;
padding: 0;
position: absolute !important;
width: 1px;
white-space: nowrap !important;
}
Using this option adds some complexity for screen/keyboard users, so this architectural decision should be considered early in the process. A requirement detailing the decision has been added to the Structure and Transformation group.
- Sublists should be hidden from the screen, but available through the DOM when they are closed.
Next Steps
I've created a repository, accessible-react-navigation-demo, to store the code that will be built. It's built on Yarn 4.1.2, Next.js 16.x, and React 19.x, using TypeScript. The code will be released as progressive enhancements, building off the prior release and adding functionality and accessibility throughout. Each release will be linked to an article that provides links to the code for that release, along with examples.
Why release this as a demonstration series rather than a third-party library ready for integration? I firmly believe that learning to work with accessibility is a process, and what better way to learn than to go through it?
Besides, I'm using third-party base components and hooks as well as a simplified, still-rough version of the theming system I'm currently building for my website, which likely aren't in your libraries.
The initial repository setup included everything necessary in package.json and configuration, and added code to implement testing using Jest and react-testing-library. I plan to aspire to 100% code coverage (with judicious use of ignore commands).
Before a clickable prototype can be delivered, any components that render HTML elements must be imported and updated to verify they meet accessibility requirements and to make it easier for developers to add accessibility while coding.
There's still more to get through before we begin coding the navigation component. I want to set this project up for success, and that means preliminary work must be in place before coding starts.
Top comments (0)