I saw a tweet recently from Heydon Pickering that offered commentary on a web development topic someone made on Reddit.
The question:
“Switch between two pages on the client side within a multi page website?”
Heydon’s suggestion:
“I have no idea but I feel like it should be easy. Trivial. Perhaps even built into browsers themselves. You could have an HTML element specifically for it. One starting with ‘a’ perhaps, so it is easy to find.”
While it is a bit snarky, it made me chuckle. There’s definitely truth to Heydon’s sentiment.
When you build a web application that uses client-side routing, you’re taking control of navigation away from HTML and the browser and giving it to a JavaScript component. This has implications for accessibility.
Screen readers rely on events like page title changes to announce that navigation has occurred. Since a JavaScript-rendered page change won’t refresh like a traditional HTML page, screen readers won’t automatically read out a title or document summary like they normally would.
If for some reason you must use client-side routing, using a library like React Helmet (or similar scripting functionality) to make changes to the document <title>
when navigating with a JavaScript library helps in this regard. This can also give us something to write automated tests for, such as “it updated the page title” with Cypress.
Focus management for keyboard users is another problem. When a navigation event occurs, JavaScript can’t exactly recreate the HTML page refresh experience, which resets focus to the top of a page along with configurable screen reader announcements. Focus could be moved to the new content with JavaScript for keyboard ergonomics-sake. I did some research on this topic, which is often referenced by library authors and spec writers but the situation is still a bit of a mess.
If client-side routing libraries try to guess for us and move focus to the same place for every situation, it can lead to a sub-par experience. This is why libraries have largely punted on addressing accessibility in client-side routing. To solve this, we need routing APIs that allow us tap into navigation events and handle focus management and announcements gracefully.
I ran into this recently while building out the Mega Nav component for the Testing Accessibility demo application. When testing menu interactivity and navigation, I found that client-side navigation with Reach Router wasn’t properly resetting the menu state while simultaneously jumping focus too far down the page automatically. Adding some code to reset menu state was an easy enough fix, but APIs for managing focus left more to be desired. In this case, using the trusty <a> tag instead of a special <Link> component allowed the browser to take care of both issues itself.
While it depends on the type of application you’re building, my general suggestion for the moment is to let pages refresh if you can. Doing this ensures that the focus point is reset, and that Assistive Technology users aren’t left wondering if anything happened. This practice will still play nicely with the dynamic updates from React Helmet, too.
There are a lot of accessibility wins we can get for free from the HTML elements we choose and the browsers the render them. It’s just a matter of knowing what to look for.