A modal dialog is a window overlaid on top of the main application. It’s an inert window where the user can’t interact with the rest of the application.
Creating a modal in React is fairly easy. There are dozens of resources that explain how to do so, including the React documentation. This article provides an in-depth step-by-step guide to building a modal component that is accessible and reusable. We will follow the official W3C requirements for building an accessible modal dialog with a reusable API.
Let’s approach our task with a few iterations and try to meet the following requirements:
For the sake of this demo, let’s create a simple parent component, App.js, that will contain our modal component and a button to trigger it:
In this iteration, we want to make the simplest possible version of a modal and get it open when clicking on the open modal button.
Our component has two div elements:
The component uses the createPortal function from react-dom . This allows us to render the modal directly in the body of the document. This makes sense for screen readers as well, since the modal should be considered outside of the main application content.
To show/hide the modal, we simply render it conditionally based on the isModalVisible boolean. When we click on the open modal button, we just set the boolean to true which will show the modal.
In this iteration, we want to make it possible to close the modal. The W3 design pattern for the dialog recommends having at least one button to do so. We want to have the following 2 buttons to do that:
To do so, we have to refactor a bit our modal so that it exposes:
By doing so, we make the Modal component very easily reusable throughout the app, since we can quickly have a working modal just by using the components it exposes.
Let’s first look at the App component. To close the modal, we pass to the modal component the onModalClose function, which sets the isModalVisible boolean to false in the parent component of the modal. Since the modal is conditionally rendered based on this boolean, setting it to false will close the modal dialog.
Now let’s look at the Modal component. Since the close buttons can be in any sub-component of the modal (header, body, or footer), we create a modal context that exposes that function. Any component that needs the function can just consume it through a context consumer. In this case, both the header and footer of the modal subscribe to the context to consume the function.
W3 recommends adding at least one keyboard shortcut to close the modal. The shortcut is the ESCAPE key.
To do so, we use the useEffect hook in order to register a keydown listener on the ESCAPE key (which has a code of 27). The listener invokes the onModalClose prop function, which closes the modal. To clean up the effect, we remove the key listener.
So far we have a relatively accessible and functional modal. However, an important part of the accessibility of the modal is trapping the user focus inside of it. Once the modal is opened, we should focus the first focusable element in it. From then, pressing the TAB or SHIFT + TAB keys will only allow the user to navigate inside the modal. Let’s go back to the Modal component and implement that.
We have to add a new key listener for the TAB key. This is a good opportunity to refactor our logic for adding keyboard listeners and make it very easy to add more listeners in the future. To do so, we create a map of key codes and listeners. Each key is the keyboard key we listen to and the value is the function listener that gets called whenever that key is pressed. Whenever a key is pressed, we look for its corresponding listener from the map and call it if it exists:
// map of keyboard listenersconst keyListenersMap = new Map([[27, onModalClose],[9, handleTabKey],]);function keyListener(e) {// get the listener corresponding to the pressed keyconst listener = keyListenersMap.get(e.keyCode);// call the listener if it existsreturn listener && listener(e);}document.addEventListener("keydown", keyListener);
Trapping the focus is basically creating a circle that has two ends: the first and last focusable elements in the Modal.
The diagram logic can be summarized in 3 conditions. When a TAB key is pressed: