If you’re looking to build your React Apps with Accessibility features baked in, you might be interested in this post. In this article, I outline how to create a generic component that can be repurposed in various ways to create accessible components. Join me on a journey through code to discover how to create an accessible reusable react component.
This article covers advanced React concepts and how to utilize them to create a generic component that sets your code base up for Accessibility Success. The components covered in this article were modeled after the W3C (World Wide Web Consortium) WAI (Web Accessibility Initiative) APG (Authoring Practices Guide) Patterns found here.
Estimated reading time: 8 minutes
Some of the concepts in this article are:
- How to use props
- What accessibility features to include in the generic component
- How to use forwardRef & useRef together
- How to use Context to control app behaviors
Let’s start
Let’s talk about how to use generic components in general. The idea of a generic component is that it can be repurposed and used in various ways. The basis for this specific accessibility component is that it sets the developer up for success.
Let’s take a look at the code below:
import React, { forwardRef } from "react"
const A11yReusable = forwardRef(({
children,
id,
className,
ariaRole,
ariaLabel,
ariaModal,
onKeyDown,
onClick,
tabIndex
}, ref) => (
<div
ref={ref}
id={id}
className={className}
tabIndex={tabIndex}
onKeyDown={onKeyDown}
onClick={onClick}
role={ariaRole}
aria-label={ariaLabel}
aria-modal={ariaModal}
>
{children}
</div>
)
)
export default A11yReusable
You’ll note that this component has several props. Some of these are optional to include so that the component remains fluid and available for multiple use cases. We can add more props to this component to broaden its use cases.
For instance, I could add an aria-expanded={ariaExpanded}
prop, an aria-pressed={ariaPressed}
and so on. However, I only added what I needed to add for the specific use cases of my app. Doing this reduces code so that a developer only uses what is needed. You can find the final product here.
How to use props
The props that are not optional remind the developer what they need to include to make the component work as an accessible generic component. If I were using Typescript, I would designate only the ariaRole as not optional.
Because Typescript can lock a component into a certain usage, it may be more beneficial for your project to instead create parent components that use the same props. An example would be a Modal.tsx component that has role, aria-modal, aria-label, etc as non-optional props. Rather than creating a generic component with many optional props, creating use specific components can help maintain a code base for devs with less experience on accessible best practices.
The generic component is good for developers who are aware of the WAI Authoring Practices and who intend to build components based on these guidelines.
I use this generic component in a Menu widget, a Menu Item widget, an Alert Dialog widget, a Dialog/Modal widget, and in a Tab Panel widget. Each of these widgets uses an aria-role attribute, but they also use only some of the other props.
Setting props as optional vs not optional is a feature of Typescript. As you’ll note in the code examples this project uses regular JS. This means that currently, all of the props are optional. It takes wanting to make components accessible and knowing what to include and what to leave out to really make an accessible, generic component.
What accessibility features to include in the generic component
My generic component was built to be used in a Tab interface which loads three different widgets. The Menu, Alert Dialog, and Dialog/Modal use this component.
The Tab Interface
In the code below, note how the A11yReusable is used in 2 different applications. It is used as a Tab List and as a Region. The inner workings of this component have been removed for readability, but you can check out the full component here.
import React from 'react'
import A11yReusable from './A11yReusable'
return (
<div>
{/** The tab list use case. This is where the role="tab" buttons are. */}
<A11yReusable ariaRole='tablist' onKeyDown={handleKeyDown}>
<TabButton
id='show-modal'
ariaControls='dialog-modal'
onKeyDown={handleOnKeyDown}
>
Render the dialog modal app
</TabButton>
<TabButton id='show-alert' ariaControls='alert-modal' onKeyDown={handleOnKeyDown}>
Render the alert modal
</TabButton>
<TabButton id='show-menu' ariaControls='menu-tab' onKeyDown={handleOnKeyDown}>
Render the menu
</TabButton>
</A11yReusable>
{/** The role="region" use case. This is where the tab panels will load */}
<A11yReusable id="tab-panels" ariaRole="region" ariaLabel="App Components" tabIndex={0}>
{modalIsOpen && <DialogTab />}
{alertIsOpen && <AlertDialogTab />}
{menuIsOpen && <MenuTab />}
</A11yReusable>
</div>
)
}
export default TabPanel
You’ll note that the role=”region” component is not using the onKeyDown prop. Also note that the tab list component is only using the ariaRole and an onKeyDown props. The tab interface detailed here, needs to have specific keyboard and screen-reader behaviors.
- The component that wraps the TabButton components has a tablist role.
- The TabButton component has a role of tab
- The DialogTab, AlertDialogTab, and MenuTab have a role of tabpanel
- The TabButton has an aria-selected and aria-controls attribute.
- Using the keyboard arrow left and arrow right, and tab and shift + tab respectively on the tablist create the keyboard interaction detailed in the APG Tabs pattern guide linked above.
The Menu
The Menu component, shown below, is based on the APG guide here. See the Menu component in full here. Note how the generic A11yReusable component is implemented with tabIndex, ariaLabel, ariaRole, and onKeyDown props.
return (
<A11yReusable id='menu' tabIndex={0} ariaRole='menu' ariaLabel='A menu' onKeyDown={handleKeyDown}>
<MenuItem>Item one</MenuItem>
<MenuItem>Item two</MenuItem>
<MenuItem>Item three</MenuItem>
<MenuItem>Item four</MenuItem>
</A11yReusable>
)
Below is the MenuItem component, see it in full here. Note that this also uses the A11yReusable component:
return (
<A11yReusable ref={menuItem} ariaRole='menuitem' tabIndex='-1' onKeyDown={handleKeyDown} onClick={handleClick} >
{children}
</A11yReusable>
)
The MenuItem component uses the ariaRole, tabIndex, onKeyDown and onClick props.
The Alert Dialog & The Dialog Modal
These components are nearly identical except for their aria role. This is based on the authoring practices detailed here.
Below is the Dialog Modal based on the authoring practices:
<A11yReusable tabIndex={'0'} ariaRole='dialog' ariaLabel='A regular modal' ariaModal={true} onKeyDown={handleKeyDown}>
<div>This is a dialog element.</div>
<button onClick={handleClick}>Close</button>
</A11yReusable>
And here is the Alert Dialog:
<A11yReusable tabIndex={'0'} ariaRole='alertdialog' ariaLabel='An alert modal' ariaModal={true} onKeyDown={handleKeyDown}>
<div>There is something important here.</div>
<button onClick={handleClick}>Close</button>
</A11yReusable>
How to use forwardRef & useRef together
If we take it back to the MenuItem component, you’ll note that forwardRef is used inside of the A11yReusable component and then useRef is used inside the MenuItem (see the MenuItem below).
import React, { useContext, useRef } from "react"
import A11yReusable from "../parents/A11yReusable"
const MenuItem = ({children}) => {
const menuItem = useRef(null)
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
alert(`Selected Item was ${menuItem.current.innerText}`)
}
}
const handleClick = () => {
alert(`Selected Item was ${menuItem.current.innerText}`)
}
return (
<A11yReusable ref={menuItem} ariaRole='menuitem' tabIndex='-1' onKeyDown={handleKeyDown} onClick={handleClick} >
{children}
</A11yReusable>
)
}
export default MenuItem
The use case for using forwardRef in the A11yReusable component and useRef in the MenuItem allows an alert message to pop up and show the user what item from the menu they selected.
Put simply, forwardRef provides a way for you to create a generic component that, when utilized, can then use the useRef hook.
The A11yReusable component is a generic component that has no internal functionality. Therefore, it is a perfect component to use forwardRef on. A component that uses state, context, or other internal functionality cannot use forwardRef.
The code example below simplifies how to implement forwardRef. You’ll note that the props are the first argument and the ref is the second. You don’t need to include a return statement.
import React, { forwardRef } from "react"
const GenericComponent = forwardRef(({children}, ref) => (
<div ref={ref}>
{children}
</div>
)
)
Next in the simplified example below, you can see how to implement useRef when utilizing this component.
import React, { useRef } from "react"
import GenericComponent from './folder/GenericComponent
const thisComponent = useRef(null)
const ThisComponent = () => {
return (
<GenericComponent ref={thisComponent} >
{children}
</GenericComponent>
)
}
How to use Context to control app behaviors
The app I’ve built with this generic A11yReusable component uses Context in various ways to control the app behavior. Because it’s used throughout, I will only showcase the way in which the MenuItem is using the OpenContext component.
It is worth noting that the Tab interface has its own separate context which is utilized to show the appropriate tab when a tab in the tablist is focussed. Check out the TabContext component here. And check out how the TabContext is implemented within the App component here.
The OpenContext, which tells the app whether the state isOpen or !isOpen (aka is closed), wraps its provider around the whole app inside the index.js file shown below:
root.render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>
);
The OpenContext component, shown below, is pretty basic state management.
import { createContext, useState } from "react"
const OpenContext = createContext()
export const Provider = ({children}) => {
const [isOpen, setIsOpen] = useState(true)
const handleIsOpenState = {
isOpen,
changeIsOpenState: (bool) => {
setIsOpen(bool)
}
}
return <OpenContext.Provider value={handleIsOpenState}>{children}</OpenContext.Provider>
}
export default OpenContext
The app is either Open or its not. This Context controls whether a modal is expanded or not, and whether a menu is expanded or not.
Note below how the MenuItem uses the changeIsOpenState function to set the state to open when the enter key or the click event happens on any given menu item.
import React, { useContext, useRef } from "react"
import A11yReusable from "../parents/A11yReusable"
import OpenContext from '../context/OpenContext'
const MenuItem = ({children}) => {
const { changeIsOpenState } = useContext(OpenContext)
const menuItem = useRef(null)
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
changeIsOpenState(false)
alert(`Selected Item was ${menuItem.current.innerText}`)
}
}
const handleClick = () => {
changeIsOpenState(false)
alert(`Selected Item was ${menuItem.current.innerText}`)
}
return (
<A11yReusable ref={menuItem} ariaRole='menuitem' tabIndex='-1' onKeyDown={handleKeyDown} onClick={handleClick} >
{children}
</A11yReusable>
)
}
export default MenuItem
The enter keyboard event, or the click event will close the menu.
Conclusion
Thanks for reading all the way through this article and taking this long journey with me. Hopefully you got to see some real world use cases for advanced React concepts like forwardRef, and Context. Ultimately, if this article gives you inspiration to make components that are more accessible to more users, then my job here is done.
Photo by Tim Mossholder on Unsplash