Stop losing focus!

Improve Accessibility in Your React App By Managing Focus in Mutable Content

Leverage the component “lifecycle” with useEffect to set the active element in the DOM

Jason Lai
The Startup

--

Photo by Daniel Ali on Unsplash

When building user interfaces we follow a design, making assumptions about how the user will interact with the frontend. We display data in a certain way, provide buttons to be clicked, and update the UI according to state changes.

For accessibility, especially with interactive UI, you should not only consider what the user sees, but what a screen reader “sees” too. This is important since the screen reader will communicate to the user how to use your application. If the screen reader can’t correctly interpret your frontend, then most likely the person using it won’t either.

With mutable content, building accessible UI becomes more complex and is not just a matter of using semantic HTML. By mutable content, I mean content that can be changed in some way, usually with CRUD operations (create, read, update, delete). While dynamic changes to the UI can be announced with ARIA live regions, I have found that focus handling is often missed.

The Problem

Imagine you are navigating through a mutable list, this could be a social feed, comments in a discussion thread, or even the items in your online shopping basket. What happens when you delete an item? The element you were focused on is suddenly no longer in the DOM, so document.activeElement will shift back to the <body> element. This means the user has to navigate through the entire page again to get back to where they were. You can imagine how annoying this is for users who use assistive technology or keyboard navigation.

You can check the current active element in the DOM using the chrome devtools by going to Console > Create Live Expression (the eye icon) and typing document.activeElement.

In React web applications, programatically changing the active element is surprisingly easy to handle. I admit when looking for a solution to this problem I went down several over complicated routes until I realised the obvious — with React refs I had access to the Node API.

The Solution

An example with a TypeScript Todo List App

We can illustrate a solution to this problem with a simple todo list. In the gist below, we have a React app that stores an array of todos in state, and iterates through them to render each one with a ListItem component. There is a <form> to add todos, and a function to delete todos which is passed to the ListItem.

For accessibility, I’ve added some state to manage the display of the application status which will notify the user when the list has been modified. This can be hidden visually with css if it is not part of the design, but can still be accessible to screen readers. The role="status" attribute implicitly applies the aria-live value of polite, meaning it will wait until the user has stopped what they are doing before making the announcement, as to not interrupt them.

Handling Focus

When an item is deleted from the list, we need to prevent the document.activeElement from shifting back to the <body> so the user doesn’t have to start over. Before we can do this, we need to define the expected behaviour and any edge cases. In the context of this application, I think it makes the most sense that when a todo is deleted, it should:

  1. Shift focus to the next todo.
  2. If there is no next todo (e.g. it is the last in the list), it should shift focus to the previous todo.
  3. If there is no previous todo, it should shift focus to the parent element.

In the case 3, it may be tempting to programatically focus the user back to the create todo <input> since there are no more todos in the list. Although you could do this, I don’t believe you should. I am a firm believer in letting the user decide where to navigate, with a few exceptions (e.g. changing focus to when a modal dialog is triggered). Suddenly changing focus to somewhere completely different on the DOM can be very confusing. Even as a sighted user I despise sites that hijack my scroll or force me to navigate to places without my consent. Remember, the goal here is simply to help the user maintain their current place in list when entries are deleted, not to force them down a user flow.

In our ListItem component, we can use a ref to access the DOM Node created in the component render. Since the ref is a DOM Node, it means we have access to all the Node API properties and methods, including nextElementSibling, previousElementSibling, and parentElementNode.

Note: unlike nextSibling, previousSibling, and parentNode, nextElementSibling, previousElementSibling, and parentElementNode will return an HTML Element or null whereas the former could return other node types such as a Document Node or Text Node.

To set the document.activeElement when a todo is deleted, we can use the useEffect hook’s cleanup function, as it will execute when the component is unmounted. We also need to add a tabindex to the elements we are going to call focus() on to ensure that we can programatically focus on them to set the document.activeElement. Since <ol> and <li> are not natively focusable elements like <button> or <a> I’m applying a tabindex of -1. This means its not focusable through sequential keyboard navigation, but it can still be programatically focused to be set as the document.activeElement so that the user does not loose their place while going through the list.

.

Common mistakes

When writing your cleanup function, it may be tempting to store the nextElementSibling and previousElementSibling in a variable, especially if you are using them in the code more than once (which I am doing in the example). This can cause the focus management to not work properly as it will give you the sibling element at the time your cleanup function is defined (on component mount), not at the time that the cleanup function is executed. This means you may be trying to focus on an element that has been deleted, or is no longer the next or previous sibling of the selected element.

However, the ref which refers to the element in the component instance should not change, regardless of changes to the list. Therefore, if you access the nextElementSibling or previousElementSibling property directly on the ref at the time that your function executes, it should always return the correct element.

Conclusion

When building interactive user interfaces, it is important to always ask yourself “what would the screen reader do?” . This helps to ensure you develop the application so it can be used by all users, including those who rely on assistive technology, especially when it comes to focus management. In React, we can leverage the component lifecycle, refs, and the Node API to help us manage which node should be the document.activeElement.

I strongly recommend testing your application with a screen reader like VoiceOver, or even just navigating with a keyboard. As a sighted user fully capable of using a mouse, I often find keyboard navigation quicker and easier. An application that doesn’t cater for that just becomes frustrating.

Accessibility should not be an afterthought, or considered only needed by users with disability. Improving accessibility improves the user experience for everyone.

The full code repository for the examples shown in this article can be found at: https://github.com/laij84/mutable-list-focus

--

--