Keyboard support for accessible modals

Creating a focus-lock hook in React.

Boluwatife Fakorede
UX Collective

--

Banner image describing the story
Photo by Ryoji Iwata on Unsplash

Accessibility is one aspect of web development that gets overlooked too quickly. People with disabilities cannot use our application or struggle to get anything done.

Organizations do not realize that they cut out a considerable user base simply by not making their applications accessible, which leads to revenue loss and can risk a lawsuit in some cases.

We should rethink making our applications much more accessible.

In this tutorial, I will be covering keyboard support for an accessible modal component.

The w3.org specifies particular keyboard supports for a modal, as shown below.

w3.org specification for keyboard support for Modal
w3.org specification for keyboard support for Modal

One of the things to note is that the tab functionality must be within the dialog. The browser doesn't have an API that lets us do this. We have to handle ourselves. Else, we have the issue as below shown below;

gif describing how focus moves outside of the modal
Focus also moves outside of the modal

We can create a focus-lock hook that locks the user within the modal.

The useFocusLock can also be used for other use-cases apart from a modal, and hence it will be generic.

Let's get started;

We will grab all of the focusable HTML tags.

Focusable Tags

We then query all of the focusableTags within the modal (or wrapper) and save them in the ref current (we could have used a state).

useFocusLock Hooks

The hook function takes wrapper props which should be a ref. All the focusable Tags query is done within the wrapper element and saved in a focusableElements ref.

N.B: We don't need to pass in the wrapper as a dependency to our useEffect refs' update doesn't trigger a re-render.

InitialFocus within wrapper

Let's make sure we can change the initialFocus when we call the hook or default to the first focusable element in the modal (or wrapper component).

InitialFocus or focus on the first focusable element if no initial focus

Keyboard listeners

Now, when we call the useFocusLock hook and pass the wrapper into it, we have access to all focusableElements within the wrapper. We can handle the keyboard support tabs by adding event listeners to the first and last focusable elements.

Tab event listener to first and last focusable element

Since we want to ensure that when we Tab within the wrapper tags, when we get to the lastFocusableElement and not tabbing + the shift key, we will return to the firstFocusableElement. That way, we are within the boundaries of the wrapper focusable elements.

Also, when we shift + Tab, we will do the vice-versa. When we get to the firstFocusableElement, we want to return to the lastFocusableElement.

Also, we are making sure that the firstFocusableElement and lastFocusableElement are the current activeElement in the document.

We are also calling and cleaning up the event listener in the useEffect.

We have satisfied everything we need to meet the keyboard support for Tabs as shown below;

gif showing how tab only moves focus within the modal.
Tab only moves focus within the modal.

One more thing to handle is the escape key event handler. We can also add to focus-lock or create a separate event handler since the focus-lock hook should be reusable and not tied to modal use-case alone. I used the latter approach.

Now we have all of the parts needed to support all of the keyboard requirements fully.

The full hook we have is below;

Thank you for reading. Let me know your thought if you find this helpful.

--

--

A frontend developer, while at that, just a curious cat that loves to share what he knows.