Accessibility Improvements to Client Side Routing in Gatsby - Available in 2.19.8
February 10, 2020
Note: This was originally posted on Gatsbyâs blog
We recently released accessibility improvements to client side routing in Gatsby core, building on previous focus management improvements released in version 2.13.2. These improvements enable people relying on screen readers to successfully navigate sites built with Gatsby. If youâd like an in-depth look at how we made incremental changes to get to this release, have a look at the conversation in this issue about assistive technology and navigation. It was opened way back in May 2018.
In July of 2019, Marcy Sutton teamed up with Fable Tech Labs to conduct user testing to determine the best user experience for navigating JavaScript applications. Marcy wrote a thorough blog post about that research. This left us with some concrete recommendations to execute on:
- Provide a skip link that takes focus on a route change within the site, with a label that indicates what the link will do when activated: e.g. âskip to main navigationâ.
- Include an ARIA Live Region on page load. On a route change, append text to it indicating the current page, e.g. âPortfolio pageâ.
Some Background
Our first step to addressing focus management (the first recommendation, above) was switching to @reach/router
. This got us part of the way there out-of-the-box. However, Gatsbyâs implementation of @reach/router
isnât idiomatic in that Gatsby puts everything on a single, catch-all route. This means that every page is technically on the same route and route changes werenât getting picked up by @reach/router
. Our improvements in 2.13.2 made sure that every time a route changed, we reset focus on a wrapping div
. These changes also ensure that our single, catch-all route is dynamic so we can register changes and take advantage of @reach/routerâs
strengths. Both of these changes were a major improvement for most users, but because some assistive technologies (NVDA and VoiceOver in particular) were still not working, we kept issue 5581 open and continued to make incremental changes over time.
While this work prioritizes screen reader users, we are still far from the most accessible solution for other disabilities. For example, users who rely on magnification, voice navigation users, and users relying on switches (devices that replace the need to use a keyboard or a mouse) have a hard time orienting themselves on a new page if focus is set on too large of an element or an inoperable element (like our wrapper div
). Sending focus directly to a smaller, interactive control like a skip link is ideal. Unfortunately, weâre limited with what we can programmatically achieve at the framework level (we have no way of knowing if a skip link exists on site pages from our Router
).
For this reason, we recommend that developers take control of focus themselves and assert the functionality in automated tests. We encourage you to take advantage of @reach/routerâs
skip nav functionality (or implement a skip link yourself) on your site.
//in layout.jsimport { SkipNavLink, SkipNavContent } from "@reach/skip-nav"import "@reach/skip-nav/styles.css" //this will auto show and hide the link on focusconst Layout = ({ children }) => {return (<><SkipNavLink /><header><h3>Welcome to my site</h3></header><SkipNavContent/><main>{children}</main><footer>© {new Date().getFullYear()}, Built with{` `}<a href="https://www.gatsbyjs.org">Gatsby</a></footer></div></>)}
Then you can make sure focus is directed there in your gatsby-browser.js
file:
// in gatsby-browser.jsexports.onRouteUpdate = ({ location, prevLocation }) => {if (prevLocation !== null) {const skipLink = document.querySelector('#reach-skip-nav')if (skipLink) {skipLink.focus()}}}
Note: After noticing that Gatsbyâs .org site was missing this, I whipped up a Pull Request to facilitate a design discussion and work through real-world feedback. Incremental improvements are so important!
New Improvements
The changes that were shipped in PR #19290 address the recommendation to add an ARIA live region that announces route changes. Using @reach-router
alone got us most of the way there depending on which browser and screen reader combination someone is using; for most combinations, page content would be communicated when changing routes. However, we found that two of the most popular combinations (NVDA with FireFox and VoiceOver with Safari) werenât announcing anything at all on client-side route changes. This leads to a confusing experience where users are unsure which page they are on and unsure if links are working. Implementing our ARIA live region ensures that there is consistent and reliable behavior regardless of the technologies used.
Our solution appends a RouteAnnouncer
component as a sibling of our main focus wrapper.
<React.Fragment>{this.props.children} //gatsby-focus-wrapper<RouteAnnouncer location={location} /></React.Fragment>
The RouteAnnouncer
is a component that renders an ARIA live region. This region has aria-live
set to assertive
because we want route changes to always interrupt whatever the screen reader is currently doing. Weâve also set aria-atomic
to true because we want every change to the content of this div
to be announced. Our ARIA live region has inline styles to hide it visually, as recommended by WebAIM.
<divid='gatsby-announcer'style={{position: `absolute`,top: 0,width: 1,height: 1,padding: 0,overflow: `hidden`,clip: `rect(0, 0, 0, 0)`,whiteSpace: `nowrap`,border: 0,}}aria-live='assertive'aria-atomic='true'ref={this.announcementRef}></div>
This component sets the content to be announced (e.g. âNavigated to Gatsby Blogâ) by targeting the innerText
on the gatsby-announcer
div, selected by ref
. Using a React ref
and only updating the announcement text if it is different from the current announcement text prevents screen readers from repeating announcements if the page renders multiple times.
One limitation of implementing this at the framework level is that we donât have access to what ultimately ends up on the pages, as they can be sourced from anywhere. For this reason, the announcement will always start with âNavigated toâ, followed by either the content of the first h1
on the page, the title
of the page, or location.path
depending on what is present. Additionally, the differences between framework level and âuserlandâ changes were evident when testing behavior compared to sites implementing similar changes themselves (e.g. Marcy Suttonâs example solution as part of her gatsby-a11y-workshop) and finding that the framework-level implementation had less consistent behavior and bugs with repetition.
Whatâs Next
Now that this large improvement is shipped, weâll continue building on our progress. Right now the English words âNavigated toâ appear in every announcement. Because accessible solutions are meant to be understandable, we aim to localize this string based on the language in which a user is navigating the web in (see issue 20801). Additionally, we would like to offer additional customization for users, offering the option to specify an element to grab announcement text from instead of the h1
or title
on the page (see issue 21059).
As always, weâd love to hear ideas and suggestions from the community on existing and/or new issues in the Gatsby GitHub repo.