Handling Focus on Route Change in React

For developers who want to:

  • Understand some of the focus problems caused by client-side rendering
  • Learn a simple technique to restore context for assistive technology users

Server side vs Client side rendering

At its simplest level, server-side rendering means that when you navigate to a new route e.g:


the server is contacted to request the page to display, and a whole new page is presented in the browser. Client-side rendering, on the other hand, means that both:

upyoura11y.com, and


are actually the same page (index.html), but the client app decides what content to drop into that single page at runtime.

In reality, there is a bit more to it than that, especially with new server-side rendering techniques, but the key to understanding focus management in React is to understand that when a user clicks a link to go to another route in your app, the DOM is manipulated at runtime, and the content of your single page is altered. Your user never actually “leaves” the page.

There are a few accessibility concerns this causes. One relates to the page title and is covered in another article on Handling page titles in React, and another is the way focus is handled for users of assistive technology.

Quick demonstration

Imagine the following scenario - as a screen reader user, you read a link to another page. You click the link using the keyboard commands. What do you expect to happen?

In “server-side rendering” land, what would happen is:

  • An entirely new page would be loaded into the browser
  • The focus of the page would be reset
  • The new page would be announced

But as we know, with client-side rendering like in React, we won’t receive a new page.

To demonstrate what does happen, turn your screen reader on, tab to this link and click on it using the keyboard. Try to then use your screen reader to navigate other content on that page. (Please come back to this page when you’re done!)

Go to demonstration page

Did the behaviour meet your expectations? What is more likely to have happened is:

  • Your screen reader informed you that you pressed the link
  • The new content was fetched and populated in the UI
  • Your screen reader did not announce anything to you about the new content
  • The focus remained on the link on this page, even though it was no longer visible

Try to imagine how disorientating this would be for a user with a visual impairment. How can they know where to begin on this new page of content? How can you be sure that they are consuming your content in the way you intended?

Potential solutions

There are a few ways to attempt to solve this problem, all involving manually manipulating the focus on the page when the new content loads. The question then is: where do we set the focus when the new ‘page’ loads?

Recently, GatsbyJS posted an interesting article summarising some user-testing of these techniques. I recommended reading their post in full, but spoiler alert:

Focusing on a heading was found to be the best experience as it would save time and make it clear what happened

A very minimal example

To demonstrate the approach that was found to be the best in GatsbyJS’s user tests, try turning on your screen reader again, and clicking the new link below. Again, note what is announced by your screen reader, and try to navigate some of the new page’s content before coming back here:

Go to solution demo page

How did your experience compare to the last demo? The implementation works by:

  • Inserting the h1 element into the tabbing order, and adding a ref to it
  • In componentDidMount() focusing that h1 using the ref
  • Disabling the default focus highlight to prevent the focus being visible other than to screen readers

As the header receives focus immediately upon load, the context of the page is immediately clear to the user, and the focus is in a position which allows interaction with the content in the order it would be expected.

Here is the relevant React code:

...waiting for Gist...

And the CSS to disable the focus highlight:

...waiting for Gist...