skip to content
Video cover art for Secrets of Accessible Routing with RedwoodJS Core Team
Video

Secrets of Accessible Routing with RedwoodJS Core Team

RedwoodJS core team members Anthony and Dom discuss accessibility improvements to Redwood's router for announcing page navigation to screen readers

Open .md

Episode Description

Dom and Anthony from the RedwoodJS core team demonstrate how single-page app routing breaks screen readers and how Redwood now fixes it automatically.

Episode Summary

This episode of Semantics explores a critical accessibility gap in single-page applications: when users navigate between routes, screen readers receive no feedback that the page has changed, leaving users disoriented. Host Ben Myers is joined by Dom Saadi and Anthony Campolo from the RedwoodJS core team to demonstrate the problem live using VoiceOver on macOS, contrasting the silent navigation of a Redwood blog app with the native announcements users receive on traditional multi-page sites. They first build a custom Route Announcer component from scratch using React hooks, a live region with aria-live="assertive", and Tailwind's sr-only utility class, illustrating both how the fix works and how much overhead it demands from individual developers. They then reveal that Redwood's upcoming v0.28 release bakes this behavior directly into the framework's router, requiring zero code changes from developers — just an upgrade. The episode also covers Redwood's new RouteAnnouncement component for overriding the default H1-based announcement, the fallback chain of H1 → document title → URL, and the research by Marcy Sutton and Madeleine Parker at Gatsby that informed these decisions. Dom and Anthony close by discussing future work around focus management, skip links, and accessibility linting, emphasizing that frameworks should shoulder these responsibilities so developers can focus on their applications.

Chapters

00:00:00 - Introductions and Project Setup

Ben Myers welcomes Dom Saadi and Anthony Campolo from the RedwoodJS core team and gives a brief overview of what Redwood is — a full-stack React meta-framework. Dom shares his role in documentation and code contributions, while Anthony explains his position as core developer advocate. Anthony notes he appeared on the very first Semantics episode and wanted to give Dom a platform to discuss his router accessibility work.

The conversation shifts into a walkthrough of the demo project, a simple blog application built from Redwood's canonical tutorial. Anthony highlights key Redwood concepts like cells for data fetching with GraphQL, and the group begins scaffolding the project using Yarn workspaces, Prisma migrations, and database seeding to prepare for the live demonstration.

00:08:19 - The Routing Accessibility Problem

Dom introduces the core issue: clicking links in the Redwood blog visually changes the page, but screen readers provide no feedback whatsoever, leaving users unable to confirm navigation occurred. Ben demonstrates this live with VoiceOver, showing that clicking the "About" link produces complete silence from the screen reader, then contrasts it with a traditional multi-page site where navigation immediately announces the new page title.

Anthony explains the technical reason behind the problem — single-page applications use JavaScript to swap content rather than requesting new pages from a server, so the browser never triggers the native page-load event that screen readers depend on. Dom emphasizes that SPAs have actually regressed from the accessibility that traditional websites provided for free, and that without addressing this, developers are essentially dropping users into new content with no orientation.

00:13:49 - Building a Custom Route Announcer

Dom walks through building a Route Announcer component from scratch using Redwood's generator CLI. The component uses the useLocation hook to track the current URL, a useEffect to respond to path changes, and querySelector to grab the page's H1 text content for the announcement. The announcement is delivered through an ARIA live region set to "assertive" with aria-atomic="true", ensuring the screen reader interrupts to announce the full navigation message immediately.

They place the component in the blog layout so it renders on every page, then demonstrate it working with VoiceOver — the live region successfully announces page titles like "About the Redwood Blog." Ben shows the Tailwind sr-only utility class to visually hide the announcer while keeping it accessible, and Dom notes the many edge cases this manual approach doesn't cover, such as missing H1 elements, multiple layouts, and pre-rendering compatibility.

00:29:19 - Redwood's Built-In Solution

Dom reveals that Redwood's v0.28 release (available immediately via the canary tag) bakes route announcement directly into the framework's router, requiring zero code changes from developers. They upgrade the demo project live, comment out the custom Route Announcer, and confirm with VoiceOver that navigation announcements work automatically. Anthony frames this as the value of an opinionated framework — solving cross-cutting concerns like accessibility at the framework level so individual developers don't need specialized knowledge.

Dom then demonstrates the RouteAnnouncement component, which lets developers override the default H1-based announcement with custom text and a visuallyHidden prop. He explains the fallback chain: the router first checks for a RouteAnnouncement, then the H1, then the document title, and finally the URL, ensuring something is always announced. The most specific announcement in the component tree wins, allowing page-level overrides of layout-level defaults.

00:40:34 - Research, Attribution, and the Road Ahead

Dom traces the journey of building this feature, crediting David Lehr for filing the original issue flagging Redwood's inaccessible router as a critical bug, and Marcy Sutton and Madeleine Parker at Gatsby for publishing the foundational research and implementation that the broader ecosystem — including Next.js — has built upon. He also credits Kyle Voss's Next.js PR for its closer architectural parallels to Redwood's router and the use of React portals for better decoupling and testability.

The conversation turns to future work, including focus management, skip links, accessibility linting rules, and potential checks on announcement quality similar to git commit message linting. Ben and Dom discuss the particular difficulty of managing focus in React and finding a one-size-fits-most approach at the framework level. Anthony invites community contributors to get involved, and the episode closes with a plug for Redwood's documentation efforts, the new merch shop, and a preview of next week's episode on the GOV.UK design system.

Transcript

00:00:01 - Ben Myers

Howdy, howdy, y'all. Welcome back to Semantics. I'm Ben Myers. Today I'm joined by Dom and Anthony. Hello, friends.

00:00:09 - Dom Saadi

Hey. Happy to be here.

00:00:10 - Ben Myers

Happy to have you all. Dom and Anthony are both from the RedwoodJS core team. Redwood, if you're unfamiliar, is a tech stack — a meta-framework for building full-stack applications in React. It's got a lot of really cool stuff going on and Dom and Anthony are part of the team that makes that happen. I want to give you all a chance to introduce yourselves for the audience. Dom, would you go ahead and start for us?

00:00:39 - Dom Saadi

Yeah. Thank you. As Ben said, I'm on the RedwoodJS core team. I mostly do documentation and some code fixes here and there, my latest being what we're going to talk about today, which I'm excited for. I also do some freelance work for Everfun, which is one of the first RedwoodJS startups, trying to make donating money easier for charities — because they shouldn't have to be doing web development. They should just do the work they wanted, the good stuff they want to see in the world. Thank you.

00:01:09 - Ben Myers

Absolutely. Anthony, welcome back to the stream.

00:01:14 - Anthony Campolo

Hello, everyone. My name is Anthony. I am also on the Redwood team. I'm the core developer advocate and I work for a company called Stepzen as well. I was, as Ben said previously, actually on the very first Semantics episode — my claim to fame. I'm super happy to have got that title. And back here mostly just to help facilitate getting Dom on here. I saw the work he was doing with the router and I really wanted to give him a platform to talk about what he's doing. I think it's really important and cool work, and if people are curious about my background, you can go check out that other episode. So, yeah.

00:01:55 - Ben Myers

All right, well, thank you all so much. And while I'm sharing links, go follow Semantics on Twitter. I'm working on putting together a website for the stream that'll have the schedule, but in the meantime the best way to find out what streams are upcoming and when they're happening is through Twitter. So please go follow. If you're in the chat, we would love to know who y'all are — feel free to give us a wave, come say hello. I see Matt is in the chat. Welcome, Matt. Good to have you. So we are going to be working on a Redwood project. In the interest of time, we did scaffold this project out ahead of time. I'm going to put the link to that repo in the chat. If you were to follow the Redwood documentation and spin up a Redwood application, you would get something a lot like this. The main exception is that Dom has very graciously gone in and added a few blog posts just so we can have some pages to work with. Dom, would you show us around this code base a bit?

00:03:10 - Dom Saadi

Yeah, absolutely. If you have done Redwood Tutorial 2, Redwood's Revenge, then you'll find this repo to be right at home. There's just a few pages. If we're looking at the file system — does that show up for me tabbing around in the pages here? The homepage right here just has the blog posts. We'll see all our blog posts as a preview and then we can click around to see the actual blog posts. It's just going to be a very simple Redwood Tutorial 101.

00:03:50 - Anthony Campolo

If you hone in on the blog post cell real quick — I think that's the one where, for people not familiar with Redwood-isms, this is basically how we're doing the data fetching. If you go to that component, it's just a GraphQL query here for our posts, and we're grabbing the ID, title, slug, body, createdAt, and then we have these different states that our data could be in, whether it's empty, loading, or failure. Then we're getting our success state, which is just spitting out the blog post. And once you go to the blog post component, people can see that too. So we're pulling in the blog post component — there's a decent amount going on here, but if you've ever seen Tailwind, that's the styling. And then we've got our router here which is linking to the different blog posts. The router is really what we're here to talk about. Setting the context of what is the problem with routing, how does that relate to accessibility, how does that relate to single-page apps — that could be a good place to start before we dive too much into the project.

00:04:59 - Ben Myers

Absolutely. I'm going to quickly spin up this project. If you were following along at home, check the README — there are some very thorough steps for how to get set up. I'm going to start by installing all of the dependencies. "Dependencies: Node is incompatible with this module." Oh, I have got an old version of that — that's fun. Let me —

00:05:24 - Anthony Campolo

Yeah. npm —

00:05:26 - Ben Myers

Yeah, let me try npm install.

00:05:31 - Anthony Campolo

You're not going to be able to npm install it. You're going to need yarn.

00:05:34 - Ben Myers

Okay, then let me bring over my terminal because I do have nvm use 14.

00:05:47 - Dom Saadi

Nice.

00:05:48 - Ben Myers

Okay, that should do it.

00:05:49 - Dom Saadi

Yeah.

00:05:50 - Ben Myers

And then yarn.

00:05:54 - Anthony Campolo

Killer.

00:05:56 - Ben Myers

All right.

00:05:57 - Anthony Campolo

Just for anyone who didn't understand what just happened there: we just have to make sure we're in the right version of Node, because we're using yarn and yarn workspaces since we're in a monorepo setup. Anyone who's ever gone down the monorepo train before — npm did actually just add workspaces. It's so you have your front end and your back end as separate projects contained within one larger project. It requires more specific CLI tooling.

00:06:33 - Ben Myers

All right, after this we have to — I'm going to do this back in the VS Code terminal. Assuming this works: yarn Redwood Prisma migrate dev. Okay, I think the issue here is actually just VS Code's terminal. Let me scooch back and forth, I guess.

00:06:59 - Anthony Campolo

Yeah. We won't need a ton more commands.

00:07:02 - Dom Saadi

Yeah.

00:07:03 - Ben Myers

Yep. I think the last one is Prisma database seed — yarn Redwood Prisma db seed.

00:07:19 - Anthony Campolo

Yeah.

00:07:20 - Dom Saadi

These commands will just get us the blog posts ready to go so we can tab around them on that blog post page.

00:07:27 - Ben Myers

Maybe.

00:07:28 - Anthony Campolo

yarn rw dev.

00:07:32 - Ben Myers

Got it. Thank you. Thank you.

00:07:33 - Dom Saadi

Super appreciate that.

00:07:34 - Anthony Campolo

We'll have you as a Redwood developer before you know it — you'll have these internalized very quickly. It doesn't take long.

00:07:41 - Dom Saadi

Yeah, you'll do it everywhere and be surprised why it isn't working anywhere else.

00:07:47 - Anthony Campolo

Let's just kick open our project on localhost:8910 and it'll be a simple blog. As Dom was saying, it takes you where you'd end up in the tutorial. This is the canonical Redwood tutorial blog — how most people get introduced to it. It's what we usually use for demonstrations, or if we want to explain or demo something with the framework. It's usually the Redwood blog.

00:08:19 - Dom Saadi

Yep.

00:08:19 - Ben Myers

All right. Dom, do you want to work us through the problem we're going to be solving today?

00:08:27 - Dom Saadi

Yeah, absolutely. Everything looks like it might be fine and working. If we go click on "About" or "Welcome to the Blog," it loads and we get to read that whole blog post. Obviously we can see that things are changing, that we've navigated to a new page. But if we actually turn on a screen reader, we won't have any indication that we've navigated anywhere new — we'll basically just hear silence. That's actually super jarring, given the fact that we've done something pretty significant on the website: changed the content in a very strong way. Not giving any indication that that happened is basically picking people up and dropping them off somewhere and letting them fend for themselves. That's not okay.

00:09:22 - Ben Myers

Absolutely. I'm going to give a quick demo of that just so we can see it in action. VoiceOver is a little slow to turn on sometimes, but I'm turning on VoiceOver. The command for that is Command-F5. VoiceOver is macOS's built-in screen reader.

00:09:45 - Screen reader / demo voiceover

VoiceOver on. System Preferences, Accessibility window. Leaving Redwood root. About. Visited link. List, two items. You are —

00:09:54 - Ben Myers

So I'm focused on the About link. I'm going to click About — maybe it's Enter. There we go. So I click Enter and the page changes, but there was absolutely no feedback. The screen reader didn't do anything about it. The user clicked the link, they totally did, but they're just kind of waiting for that input: did things change? Do I need to click it again? What do I need to do? That's a problem — that's not an anticipated experience.

00:10:27 - Dom Saadi

Yeah. We're leaving them with no choice but to tab around, basically reading the whole page out loud just to know if something changed. That's so much overhead, and we should be doing something about this. Because with regular, non-single-page-app sites, this behavior is handled for you. So while we think we might be moving forward with SPAs, we've actually left something behind that we'd get natively. In a sense, we're moving backwards if we don't handle this behavior.

00:10:58 - Anthony Campolo

I think it's important to drill in a little bit on why this is happening. If you haven't really gotten deep into what a single-page application is: it's just a bundle of JavaScript handling your whole project. When we're switching pages, we're executing JavaScript that tells the browser to go to those different pages — unlike in the past, where every link would send a request to the server and serve up a new page. So as Dom was saying, the server serving up a new page would give the screen reader the information it needs to know navigation occurred. But because we're in this JavaScript-only world, we can't just assume that behavior. It's all happening in a JavaScript black box, and we need to figure out how to tell screen readers that we have navigated.

00:11:53 - Ben Myers

Very well explained. As Dom called out, on static pages — good old HTML pages — the experience works as anticipated. Let me turn off VoiceOver real quick. I'm going to go to what should be a static site, which would be the WebAIM site. If I turn VoiceOver back on and click one of the links, we'll get some feedback. You'll be able to see the native experience the browser gives us.

00:12:29 - Screen reader / demo voiceover

System Preferences. Accessibility. Chrome. Visited link. Image. WebAIM. Visited link. Services. List, five items. WebAIM — collaborating with WebAIM. Web content —

00:12:39 - Ben Myers

As we navigated to this page, it announced "WebAIM" or "WebAIM — collaborating with WebAIM." We clicked the link, we opened a new page, and immediately we got feedback. We were told the name of this new page. This is the experience we get out of the box, for free, when we're using regular browser links. Because single-page applications don't do a hard page load — they substitute the contents of the HTML page instead — the screen reader has no such input and can't announce navigation to users. So that's what we're going to address today: how Redwood, and many other React frameworks, are enabling us to solve this problem. Let's go ahead and get started. If I were a Redwood developer and I wanted to address this — I wanted it so that when I click "About" it announces something to the user — what would I have to do for that?

00:13:49 - Dom Saadi

Yeah, it's a tough problem, and as a developer one of the things Redwood wants to take care of is all the stuff that distracts you from your app logic. This definitely feels like something you'd have to do yourself. What would you do? I would start by generating a component — I'd run one of our generators: yarn Redwood generate component. Let's call it RouteAnnouncer, because the behavior we want is: on page change, it should go ahead and announce the new location.

00:14:23 - Ben Myers

I think we're going to have to do this in my other terminal, but let me just copy-paste this.

00:14:28 - Dom Saadi

Yeah, no worries.

00:14:30 - Ben Myers

So: yarn Redwood generate component RouteAnnouncer.

00:14:35 - Dom Saadi

Okay, that's going to give us a new component called RouteAnnouncer in components. I'll go ahead and navigate there. Oh, awesome. Thank you. I'm going to add some comments here so we can think about this.

Basically, when the URL changes, we want to say something. At the very least, the URL — in a React app the current location is usually called a "location." The first question is: how do we get access to that? Luckily there's a hook we can import from the Redwood router package, @redwoodjs/router, called, aptly, useLocation. We can go ahead and use that here. We have the location now. It's going to have a property called pathname — which is where we are, like /about or /contact. This seems like something we want to sync up with a side effect. Hopefully that word gave it away: we need to use a useEffect hook, because we want something to happen when the location changes.

00:16:01 - Dom Saadi

I know that this location has a property called pathname. I know when that changes I want to do something. Now I want to announce — but what does that mean? We'll need some state, so we can use useState too, and call it announcement and setAnnouncement. In here we want to set the announcement with some kind of data. We could announce location.pathname, but that's not always very readable — that could sometimes be something like blog-post-13, and that's not going to give the user a good indication of what the page is. It might be better if we look for the H1 on the page, which we can do with querySelector. We'll get that HTML node and read its textContent — whatever text it wraps.

00:17:23 - Dom Saadi

Maybe I'll go ahead and do that here: setAnnouncement(...). Obviously we're assuming an H1 is going to be on the page, so if it's not there we're going to have to handle that — which is already some overhead to think about. Now the question is: I found the announcement text, so how do I actually announce it? This is where ARIA live regions come in. A live region is what the screen reader will look for to announce something. We give it aria-live="assertive", which means "say this as soon as you can" — it'll interrupt the screen reader. If it's in the middle of saying something and this gets changed, it'll stop and announce this instead. That's the property we want, because it's super important that the user knows they've gone somewhere new. There's one more property to add: aria-atomic.

00:18:36 - Dom Saadi

We set that to true — which means "announce the whole thing, even if only part of it changed." That's useful if we have more in this string — like if we interpolated and said "Navigated to [announcement]," it would announce the whole string, not just the new announcement value. And this might already be enough to get us something. The question now is: where do we want to put this?

00:19:22 - Anthony Campolo

Let me pause here real quick to recap what we've done, just to make sure everyone understands. So useLocation — let's first define what that hook is doing. It's coming from the Redwood router, but it's not really doing anything fancy. We're just reaching into the browser's API and using location like you would naturally.

00:19:47 - Dom Saadi

Right, totally. There's no magic at all. Like Anthony said, you could use the native browser API too.

00:19:57 - Anthony Campolo

Yeah. The location is basically where you are on the page. So when you're clicking around, your location is changing — and that's your route in your address bar, correct?

00:20:07 - Dom Saadi

Yeah, it's the page you're on. So it won't tell you where focus is, per se, but it will tell you that you're on the home page, the about page, or a contact page.

00:20:18 - Anthony Campolo

Yeah. All you're doing is writing out a manual way of telling your program what the location is. We've written this into the application, and now we're trying to find a way to actually bake it into the framework so that every developer doesn't have to do this themselves.

00:20:37 - Dom Saadi

Yeah, and there's actually a lot more I haven't even taken care of yet — like, what if there's no H1 on the page? What do we announce then? Do we announce the title? Do we announce the location?

00:20:49 - Anthony Campolo

I'm curious, Ben — looking at this, is this a solution that you would have thought of, or that you would have seen implemented in projects with single-page routing issues? Or would you see this problem and approach it differently?

00:21:11 - Ben Myers

I think there are many different ways you could approach this. And part of the problem is that, while there has been research done on this and user testing done on this — and we will absolutely get to that later — there's been very little of it. So a lot of people have just kind of thrown stuff at the wall to see what sticks. For instance, one thing I might do to solve this is automatically focus on the H1 instead of using a live region. That's doable, but whether or not it works kind of depends on your user testing and the needs of your application. There are many different ways to approach this, and this is one very solid, generally applicable solution. Even though it seems a bit overwhelming — we're observing the location so that whenever it changes, we can search for the H1, use its contents to populate a live region, and tell the user "hey, you're on a new page."

00:22:17 - Ben Myers

This is that new page. This is a very strong solution. Dom, I believe you were going to plug this RouteAnnouncer component in somewhere.

00:22:30 - Dom Saadi

Yeah, exactly. We might think we've done the work, but we're not actually rendering it. Where's the best place to do this? It's tough because it has to render on every page. In Redwood, the best place right now is probably a layout. We'll go to the blog layout, since it wraps every page in this application. I'll import RouteAnnouncer from the components directory — I can spell properly — there we go. Then I'm going to render it up here and see what happens. That should be enough to see something at least. We might realize we forgot a thing or two once we actually see it on the page.

00:23:32 - Ben Myers

Yep. I'm spinning up yarn Redwood dev and — oh, it opened up yet another new window.

00:23:40 - Dom Saadi

Isn't that lovely? You can turn that off, actually. It's one of the first things I do.

00:23:46 - Ben Myers

Interesting. You might have to teach me how to do that at some point.

00:23:51 - Dom Saadi

Yeah, it's just a TOML setting.

00:23:53 - Anthony Campolo

Sweet. Redwood hacks for the people.

00:23:57 - Dom Saadi

So there's already a problem. Ben, you just highlighted it — we're kind of repeating ourselves here.

00:24:04 - Ben Myers

Yep.

00:24:05 - Dom Saadi

One thing we forgot to do was hide it. We don't really need this to be visible. I can jump back to the code editor and add a quick style to the RouteAnnouncer to hide it. Since we're using Tailwind, which is amazing, we're going to add the sr-only utility class. I actually didn't know they had this until very recently, but it's awesome. "SR" stands for screen reader. We don't want to hide this in the sense of making it invisible or not rendering it — there are a lot of different ways to hide things in CSS. Tailwind provides this utility so that we can keep the element there for screen readers while keeping it off the visible page. This utility class comes in handy a lot.

00:25:01 - Ben Myers

If you want to see the contents of that class, I'm willing to bet Tailwind is implementing something along the lines of this logic right here. The approach is not making it invisible per se — we're just making it really, really small. This is very likely the Tailwind implementation. I put the link to that blog post in the chat — it's such a great utility. If you don't mind, I'd like to see it without the class name first, and then we can apply it and confirm everything still works.

00:25:39 - Dom Saadi

Oh yeah, absolutely.

00:25:41 - Ben Myers

Without the class name — that's a powerful visualization of what's happening here. Let me refresh this just in case. I'm going to turn VoiceOver on.

00:25:55 - Screen reader / demo voiceover

VoiceOver on. System Preferences. Accessibility. Leaving Accessibility features. Chrome. Leaving Redwood. Visited link. About. List, two items. About the Redwood Blog. You are currently on a —

00:26:05 - Ben Myers

Okay, so as I click the link, our live region up here changed to "About the Redwood Blog," which is the content of the H1 down here. And the screen reader immediately announced it. That's incredibly cool already — we've got feedback that you actually navigated somewhere. I'm sure this works for the other pages too.

00:26:23 - Screen reader / demo voiceover

Redwood Blog. What is the meaning of life? You are currently —

00:26:28 - Ben Myers

That's pretty cool. That's really exciting. And then you were showing off the sr-only class.

00:26:35 - Screen reader / demo voiceover

VoiceOver off.

00:26:36 - Ben Myers

Let's do that real quick. So className="sr-only". We're back here, we can't see the live region, but it should still work for us.

00:26:54 - Screen reader / demo voiceover

VoiceOver on. System Preferences. Accessible. Chrome. About the Redwood Blog. You are currently. Redwood Blog. You are currently. A little more about me.

00:27:03 - Ben Myers

Yep. All seems to be working.

00:27:05 - Screen reader / demo voiceover

VoiceOver off.

00:27:06 - Ben Myers

Is there anything more you would add to this?

00:27:10 - Dom Saadi

If you were going to go live with this, it's definitely not enough. Every page should have an H1 — that's a given. But sometimes it's not ideal to announce the H1. You really want to keep the audible experience close to the visual experience of your document. Sometimes your H1 just isn't descriptive enough on its own. You're going to have to handle that case — this logic is going to get a lot more if/else statements. What's even worse: if you have a new layout on a new page, there's just a lot more overhead to think about. This is awesome that it works, but you as a developer all of a sudden have to code for all of these edge cases. And you really can't leave even one out, because if you miss a single page, you're going to have that jarring behavior of "where am I?"

00:28:14 - Dom Saadi

Did it work? Did it load? Is it still loading? It's really hard to tell.

00:28:19 - Ben Myers

Yeah, that's a really great point. And it also relies on me, as the person implementing a Redwood site, having enough accessibility knowledge to realize I need to be announcing the page title, and also knowing how to implement the solution. It relies on so much knowledge on my end for something that really should just be given out of the box.

00:28:58 - Dom Saadi

Yeah, absolutely. And I didn't even mention pre-rendering, because we're using a web API here — document — and if you're going to pre-render, it's going to throw an error because there's no document. You're going to have to code for that too. It's just a lot of extra work.

00:29:19 - Ben Myers

So in that case, what can Redwood do for us? If we're looking to really leverage Redwood's capabilities here, what would we need to do?

00:29:39 - Dom Saadi

Yeah, this really does seem like something that should be handled at the framework level, because isn't this going to affect every Redwood app? Everyone's going to have to implement their own RouteAnnouncer, and the logic isn't really going to change from app to app — we need to announce something descriptive. Wouldn't it be nice if this were handled at the router level? The good news is: in v0.28, which is coming out at the end of the week, that's going to be the case. And if you love to live on the bleeding edge, you can actually just upgrade now and your pages will be announced automatically. There's nothing you have to do.

00:30:19 - Ben Myers

Okay, so what do I need to do to make this work?

00:30:23 - Dom Saadi

You have to run yarn Redwood upgrade and then add a tag: -t canary — like the bird. All of the latest and greatest Redwood is in the canary channel. Then we just have to wait for node_modules to install, which it always does.

00:30:47 - Anthony Campolo

Nothing ever goes wrong with Node.

00:30:51 - Ben Myers

Never, never. After this Friday — after this week, whenever the canary goes live — that would probably be a different command, right? It wouldn't be -t canary.

00:31:05 - Dom Saadi

Oh yeah. It would just be yarn Redwood upgrade with no arguments.

00:31:10 - Anthony Campolo

And then if you were to generate a new Redwood application once 0.28 is out, you will just get this automatically. This is what we talk about with being an opinionated framework, or a framework that values convention over configuration. These are the opinions, these are the conventions you get from using a framework like Redwood — because we have a team of people thinking about these higher-level issues and trying to solve them at the framework level, for Redwood developers who may or may not have this kind of experience. You'll see it said "esbuild" there — that's exciting.

00:31:55 - Ben Myers

Gonna think about this one for a while.

00:32:00 - Dom Saadi

Chat — if you've got any questions, let us know. Let us know what you're thinking.

00:32:06 - Anthony Campolo

Curious if anyone in the chat has had to deal with any of these sorts of issues. I would assume there are other developers out there who've worked on single-page applications. Going back to when I was first on the show with Ben, I was saying I was never taught any of this — it was never in my boot camp curriculum, never really prioritized. This is the type of thing I've had to figure out on my own. Being involved with Redwood is what led me to start thinking about some of these issues, because we've been talking about problems with the router — I think it was back in May when someone opened the first issue about it. So it's been a known issue within the framework, something we've wanted to figure out. I was always hearing "the router is not accessible" — and I'd think, what does that mean?

00:33:10 - Anthony Campolo

That's what we're trying to draw out here: why is the router inaccessible, and what does it take to have an accessible router?

00:33:13 - Ben Myers

Absolutely. All right — and by the way, Rob compliments you on your shirt, Dom.

00:33:20 - Dom Saadi

Oh, thank you, Rob.

00:33:22 - Ben Myers

We might have to show off the Redwood store at the end of the stream because Redwood's got some swag. All right, so we have now installed the canary version of Redwood, the stuff that should be going live later this week. What do I need to do now?

00:33:41 - Dom Saadi

So I commented out the RouteAnnouncer — that's totally gone, there's nothing in the layout that should be rendering. And then all we actually have to do is start the dev server, go click on a link, and just be happy. I'll pray that you guys can be happy when it works.

00:34:08 - Ben Myers

Fascinating. Now it opened up in a new tab. At some point I'm going to figure out the non-deterministic approach to opening up new windows and tabs that Redwood is using here.

00:34:19 - Dom Saadi

Yeah, it's a quick TOML setting. Just set it to false and it won't happen — I promise.

00:34:28 - Ben Myers

All right, so we got the page up. Let me turn VoiceOver on.

00:34:34 - Screen reader / demo voiceover

VoiceOver on. System Preferences. Leaving Scroll Open. VoiceOver Training button. Chrome. Leaving Red. Visited link. Contact. Contact. Leaving Redwood Root. About the Redwood Blog. Chrome has new window. Redwood Blog. What is the meaning of life? Chrome has new window. You are currently on a —

00:34:56 - Dom Saadi

All right now in chat —

00:34:58 - Ben Myers

There we go. Yeah, so even though we commented out our RouteAnnouncer, all of this stuff is already working for us — we're able to announce every route. So what this means, if I'm understanding correctly, is that to get this working for Redwood apps going forward, you just have to upgrade. That's the migration path. You just upgrade Redwood. You don't even need to know what all the updates do.

00:35:26 - Dom Saadi

You just have to update. No codemods, no changes to your pages or your layouts or your routes. It's just going to happen.

00:35:36 - Ben Myers

That's incredible.

00:35:37 - Anthony Campolo

Whereas with pre-render, you had to add the word "pre-render" to your [route definition]. You've taken it all the way down to zero words.

00:35:48 - Dom Saadi

Zero words. Yeah. I had to one-up Danny, you know — I just couldn't let him be that awesome.

00:35:55 - Ben Myers

That's awesome though. What an experience to be able to just update and get accessibility improvements for free, without needing to know or worry about any of it. Welcome, Chan — good to have you.

00:36:06 - Screen reader / demo voiceover

VoiceOver off.

00:36:08 - Dom Saadi

Yeah, it was really important to Tom and everyone at Redwood that accessibility be a priority for version 1. We're thinking about hitting v1 in the next few months, and accessibility is a v1 concern — it's not something we deal with after v2. This just needed to be dealt with, especially at the outset, because it's so much harder to make these changes to the router after v1. The router affects every Redwood app, so it's definitely nerve-wracking to work on, but it's super important. The changes you make there can have a lot of impact, as we're seeing.

00:36:53 - Ben Myers

All right, what more can these updates do for us?

00:36:59 - Dom Saadi

As I mentioned before, there might be a case where your H1 isn't good enough or isn't descriptive enough. Let's go to the Contact page, for example. It just says "Contact" — nothing wrong with that, but say we wanted to say something a little more friendly or welcoming to the screen reader. We can import a component from @redwoodjs/router called RouteAnnouncement — just "announcement" instead of "announcer." Then we can render it right down here on the Contact page and say something like "Get in touch with us" instead of "Contact." We don't want this to be visible on screen, so we can pass the visuallyHidden prop. Now instead of announcing the H1, the screen reader will announce whatever's inside this RouteAnnouncement component.

That's a feature we added to give you an escape hatch — there's always going to be a case where you need to do something different. The RouteAnnouncement component is the way you can still benefit from what Redwood gives you for the standard case while handling edge cases with this. So now if we navigate to the Contact page, we should hear this announcement instead.

00:38:35 - Screen reader / demo voiceover

VoiceOver on. System Preferences. Accessibility. Chrome. Leaving Red. Visited. Visited link. Contact. Get in touch with us. You are currently —

00:38:43 - Ben Myers

That's super cool.

00:38:46 - Dom Saadi

Yeah. And the good thing about that too — say you put an announcement in the layout and then one on the page itself. The Redwood router will look for the most specific one, so you can always override it at a more specific level if you need to.

00:39:04 - Ben Myers

That's awesome. That seems really well thought out. Broadly speaking, what is the chain of precedence? You've mentioned the H1, and multiple levels of route announcement — what's that chain?

00:39:19 - Dom Saadi

We'll get into where the chain came from when we talk about the research a bit later. But the chain is: RouteAnnouncement is first, so if there's one present, that gets announced. Then the H1. Then if there's no H1, it'll be the document title — the one in the <head>. And then if there's no document title, it'll be the URL itself. So something will always be announced — it will never just be silent. We want to be as specific as possible, which is why we have that order.

00:39:54 - Ben Myers

That's awesome. Really cool. I love providing the escape hatch, because nine times out of ten, your H1 should be totally fine. But in the case that you need more control, you absolutely shouldn't be fighting with your own tooling — the tooling should give you a way to say "no, I do know better, do this instead."

00:40:19 - Dom Saadi

Yeah. Otherwise you'd basically have to recreate that RouteAnnouncer component again, and that's not the experience we want for developers. We want accessibility to be easy to write. And yeah, that's super cool.

00:40:34 - Ben Myers

All right. I would love to talk through your experience building this out and what you learned along the way. Not all of us will be contributing to Redwood or building our own router, though you totally can. But I want to talk through your journey: how did you identify that this needed to be addressed, and how did you identify solutions?

00:41:08 - Dom Saadi

Yeah, it was quite a journey. Tom wanted [to make VoiceOver work with] Redwood, and because he had opinions — especially about having all your routes in one file, which I'm super grateful for — he obviously knew all the challenges that come with building your own router. Stuff like this. Because React and routers have a long history at this point, so we'd be playing a lot of catch-up. And accessibility was something Tom wanted to get done for v1.

The first person who sounded the alarm was David Lehr, who posted an issue way back in May — a year ago now. There's actually more to get into with what we're going to do next, especially around focus. But he was telling us exactly what was wrong, and to reproduce it was literally just: start any Redwood app, turn on the screen reader, there you go. He pointed me in the right direction. He works at [unclear] Labs now. Back then I just thought it was cool that people were noticing this stuff, but he's actually an accessibility expert.

00:42:18 - Dom Saadi

So he knew what he was talking about — this wasn't just someone posting an issue. He said at the bottom that this is a critical bug, that he's not even going to use Redwood because of it. People shouldn't underestimate the severity of these things, because it really does exclude — not only excludes users from navigating your website, but it excludes developers from using your framework. But he pointed me to Gatsby as the state of the art, and specifically to Marcy Sutton and Madeleine Parker. Their work at Gatsby is what I owe everything in this PR to. They shared their research and their implementation with everyone in the tech community, and did an amazing thing for open source by putting it all out there — even Next.js pulled in a route announcer based on this work. The work these two did with the research and the PR is just — hear, hear.

00:43:36 - Dom Saadi

Marcy's blog post is really thorough, almost like a scientific paper. I highly recommend reading it. She goes over all the methods she used and has two takeaways at the bottom. One is that the page should be announced — which we've implemented. The other is that a small interactive element should be focused, and she recommends that element be a skip link. Skip links are just as important as announcing the route, especially for a production website — you have to provide them. A skip link lets you skip the navigation at the top of the page, because it gets really tedious to constantly tab through the nav. That's something we'll be adding to Redwood before v1, after we do a few more router changes. Because managing focus in React is hard, to put it succinctly.

00:44:43 - Ben Myers

In the chat we have my coworker Isabel — she and I can commiserate on this. Focus is hard, and React is not well suited to solving problems of focus. That's my hot take right there.

00:44:59 - Dom Saadi

That's exactly right. But there's one more person I should give credit to, which is Kyle Voss, who did the Next.js PR. I followed his implementation a little more closely because the router had more similarities. I actually have to follow his implementation even more closely now, because he uses a React portal. At first I didn't understand why — I was like, I don't have to use a portal, I'm not going to, because I've only heard about them and that sounds really hard. But I realized it's for decoupling — so we can test the router on its own and the RouteAnnouncer on its own. Anyway, I owe it all to these four people who paved the way for making the web more accessible, and did it in such a concrete way.

00:46:05 - Ben Myers

Absolutely. And sharing what they learned. Gatsby could have totally held onto the secret knowledge of "we figured out accessible routing."

00:46:15 - Dom Saadi

Yeah.

00:46:16 - Ben Myers

And they didn't. They shared it so that anyone building single-page applications can learn from it, add to it, do their own testing. I think that's really cool.

00:46:28 - Anthony Campolo

Do you think, coming at this problem now that you've seen a bunch of different solutions — do you think future implementations could improve on what we've got, or do you think this could be considered a solved problem? What do you think?

00:46:48 - Dom Saadi

I think there's a lot more to do in terms of linting rules and tooling to make sure that your announcements are actually good. Is what you're announcing descriptive? We can add a lot more there. The implementation itself — one thing Redwood could do better when we start getting more static pre-rendering is that static pages are much better for accessibility in general, especially when you have cells and data loading. You never want your H1 inside something that might not load. So that'll help a lot down the road.

00:47:36 - Anthony Campolo

Is your ESLint PR that you have open related to the linting you're talking about?

00:47:43 - Dom Saadi

That's just for more general accessibility concerns, like not having an alt attribute on an image tag — right now that would go unnoticed, and that PR would add those rules. But in Madeleine Parker's a11y helpers PR, she goes over a lot of next steps, and one of them is adding error checking to route announcement components — for instance, checking the length of their children. You could think of it like how a git commit message gets linted: "you wrote ten words, you should rewrite that." We could do the exact same thing here, and even fail CI if you really wanted to.

00:48:34 - Anthony Campolo

That always reminds me — don't forget error handling.

00:48:39 - Dom Saadi

Yeah. Focus is probably the next big challenge for us — the skip link especially. But we're happy to get this in and give people the things they get for free when they build on Redwood.

00:49:01 - Ben Myers

Absolutely. To clarify the focus situation for people who might not be familiar: across routing in a single-page application, because you're not doing a hard page load, if there are any components React decides shouldn't unmount or update, those stay on the page — and if you're focused on any of them, your focus stays there too. For instance, going back to our page here: when I click "About," the nav bar stays put, so my focus should remain on the About link in most single-page applications. In this case it looks like it didn't, so it sounds like y'all are already doing something right there.

00:49:49 - Dom Saadi

Actually, we're re-rendering the layout, which is really bad — so we're doing something right by doing something wrong.

00:49:57 - Ben Myers

It's incredible. On many single-page applications, because the navbar usually wouldn't re-render, focus would stay there after clicking a link. So it's like you go to a new page and your focus just sticks where it was — no one expects that. It's an interesting problem to solve from a framework level, because you have to find not a one-size-fits-all approach, but a one-size-fits-most approach. That's really hard given the variety of applications out there.

00:50:36 - Dom Saadi

Yeah, it's a much greater challenge than the announcement. No one's going to argue that something should be announced. But with focus I can see where we might get more pushback — "what about this implementation?" It'll be tough to find something that works for everyone, like you said. But definitely something the framework should help you with — it shouldn't be up to you alone.

00:51:04 - Ben Myers

Awesome. Well, cool. Do you have anything else you want to add to this demo, Dom and Anthony?

00:51:15 - Dom Saadi

We'll have documentation for all this too, and for accessibility more broadly, because it's a much wider topic than what we've covered here. If you're making any React modal or anything like that, we want you to have what you need. We'll provide more resources to learn about it, and libraries to use like Reach UI or React Aria — those are two really good ones. Just know that this is just the beginning.

00:51:47 - Anthony Campolo

Yeah, we're lucky that Dom is also one of our most prolific doc writers. We're in the process of documenting a lot of what we've talked about today, and I had a preview link that I dropped in the React Podcast Discord accessibility channel that shows what Dom is working on. If anyone wants to go check that out and give feedback or help out — anyone who finds this stuff interesting — Dom is heading it up, but we're always open to contributors. Redwood is of course an open source project. We love getting people involved. If this is exciting to you and you want to be on the forefront of accessibility research in frameworks, we'd love to have you and to hear your thoughts.

00:52:42 - Ben Myers

Awesome. Well, thank you. And also — where can we get some good Redwood swag?

00:52:49 - Dom Saadi

I'm glad you asked, because there's a new shop in town. Just go to shop.redwoodjs.com and you too can look cool on stream.

00:53:03 - Ben Myers

I definitely ordered myself the black-on-black shirt that's coming in very soon. Some good stuff — go get yourself some Redwood swag. Y'all have a very classy logo, I think. Dom, Anthony, thank you both so much for hopping on. I'm absolutely thrilled that we got to show this off and show the good work that Redwood is putting forward to make Redwood applications more accessible out of the box, and talk a bit about how tooling can give us the lift we need for free — oftentimes without us even realizing it. When the magic command is "just update your Redwood version," that barrier to entry is so low, and I think that's very cool. So thank you both for hopping on.

Chat, go ahead and put it on your calendars: we're going to be back this time next week, 12pm Central Time. We're bringing on Chris Burns. Chris is going to be showing us the GOV.UK design system. I've heard great things about it, and you can imagine — as a government, building accessibility into your design system is such an important part of the process, because otherwise you legitimately exclude people from being able to participate in civil processes.

00:54:26 - Ben Myers

Right — that's a non-trivial thing. So we'll be exploring the GOV.UK design system together. You can go follow Some Antics on Twitter @SomeAnticsDev — I'll put that link in the chat. Go follow Dom too, I'll put that in as well. And Anthony — all right, you can go follow him if you want. You don't have to.

00:54:54 - Anthony Campolo

Let's go.

00:54:56 - Ben Myers

But you can totally do that. And stick around for a bit too, because I am going to find someone to raid — that's something I'm trying to get better at. So thank y'all so much and we will catch you next week.

00:55:12 - Dom Saadi

Bye.

On this pageJump to section