Skip to main content
Version: 7.4

Accessibility (aka a11y)

We built Redwood to make building websites more accessible (we write all the config so you don't have to), but Redwood's also built to help you make more accessible websites. Accessibility shouldn't be a nice-to-have. It should be a given from the start. A core feature that's built-in and well-supported.

There's a lot of great tooling out there that'll not only help you build accessible websites, but also help you learn exactly what that means.

Does tooling obviate the need for manual testing?

No—even with all the tooling in the world, manual testing is still important, especially for accessibility. But just because tooling doesn't catch everything doesn't mean it's not valuable. It'd be much harder to learn what to look for without it.

Accessible Routing

For single-page applications (SPAs), accessibility starts with the router. Without a full-page refresh, you just can't be sure that things like announcements and focus are being taken care of the way they're supposed to be. Here's a great example of how disorienting SPAs can be to screen-reader users. On navigation, nothing's announced. The lack of an announcement isn't just buggy behavior—it's broken.

Normally, the onus would be on you as a developer to announce to screen-reader users that they've navigated somewhere new. That's a lot to ask—and hard to get right—especially when you're just trying to build your app.

Luckily, if you're writing thoughtful content and marking it up semantically, there's nothing you have to do! The router automatically announces pages on navigation, and looks for announcements in this order:

  1. The RouteAnnouncement component
  2. The page's <h1>
  3. document.title
  4. location.pathname

The reason for this order is that announcements should be as specific as possible. more specific usually means more descriptive, and more descriptive usually means that users can not only orient themselves and navigate through the content, but also find it again.

If you're not sure if your content is descriptive enough, see the W3 guidelines.

Even though Redwood looks for a RouteAnnouncement component first, you don't have to have one on every page—it's more than ok for the <h1> to be what's announced. RouteAnnouncement is there for when the situation calls for a custom announcement.


The way RouteAnnouncement works is simple: its children will be announced. Note that this can be something on the page or can be something that's visually hidden using the visuallyHidden prop:

import { RouteAnnouncement } from '@redwoodjs/router'

const HomePage = () => {
return (
// This will still be visible
<h1>Welcome to my site!</h1>

export default HomePage
import { RouteAnnouncement } from '@redwoodjs/router'

const AboutPage = () => {
return (
<h1>Welcome to my site!</h1>
// This won't be visible
<RouteAnnouncement visuallyHidden>
All about me

export default AboutPage

visuallyHidden shouldn't be the first thing you reach for—it's good to maintain parity between your site's visual and audible experiences. But it's there if you need it.


On page change, Redwood Router resets focus to the top of the DOM so that users can navigate through the new page. While this is the expected behavior (and the behavior you usually want), for some pages—especially those with a lot of navigation—it can be cumbersome for users to have tab through navigation before getting to the main point. (And that goes for every page change!)

Right now, there's two ways to alleviate this: with skip links or the RouteFocus component.

Since the main content isn't usually the first thing on the page, it's a best practice to provide a shortcut for keyboard and screen-reader users to skip to it. Skip links do just that, and if you generate a layout using the --skipLink option, you'll get one with a skip link:

yarn rw g layout main --skipLink
import { SkipNavLink, SkipNavContent } from '@redwoodjs/router'
import '@redwoodjs/router/skip-nav.css'

const MainLayout = ({ children }) => {
return (
<SkipNavLink />
<SkipNavContent />

export default MainLayout

SkipNavLink renders a link that remains hidden till focused and SkipNavContent renders a div as the target for the link. The code for these components comes from Reach UI. For more details, see Reach UI's docs.

One thing you'll probably want to do is change the URL the skip link sends the user to when activated. You can do that by changing the contentId and id props of SkipNavLink and SkipNavContent respectively:

<SkipNavLink contentId="main-content" />
{/* ... */}
<SkipNavContent id="main-content" />

If you'd prefer to implement your own skip link, Ben Myers' blog is a great resource, and a great place to read about accessibility in general.


Sometimes you don't want to just skip the nav, but send a user somewhere. In this situation, you of course have the foresight that that place is where the user wants to be. So please use this at your discretion—sending a user to an unexpected location can be worse than sending them back the top.

Having said that, if you know that on a particular page change a user's focus is better off being directed to a particular element, the RouteFocus component is what you want:

import { RouteFocus } from '@redwoodjs/router'

const ContactPage = () => (
{/* Way too much nav... */}

// The contact form the user actually wants to interact with
<TextField name="name" />

export default ContactPage

RouteFocus tells the router to send focus to it's child on page change. In the example above, when the user navigates to the contact page, the name text field on the form is focused—the first field of the form they're here to fill out.