Single Page Apps routers are broken

Currently, every major Single Page App’s (SPA) router is broken the second you install it (Ember, Angular, React/Preact/Vue.js, etc). Before we go much further, I want to make sure you know I’m not hating on SPAs. The opposite, actually! I love SPAs and the benefits that come with them are amazing.

Those benefits come with tradeoffs. For example, with SPAs, you don’t have to re-render the entire page when you navigate to a new route. Only the part of the page that has changed will re-render. This has great performance benefits but leaves a gaping hole for users who rely on assistive technology like a screen reader.

When you click a link on a server rendered page it will re-render the entire page. This works great with screen readers because they know there’s a new page and they know when it has finished loading. Since SPAs don’t reload the page, screen readers don’t know new content has been loaded. This short video will get the point across:

Disqus SPA broken router example

In the video, I’m trying to navigate the disqus admin panel with VoiceOver (the macOS screen reader). You can see the route changes when I activate the “community” link but nothing is announced to the screen reader other than that you pressed a button. If you’re relying solely on what the screen reader is telling you, you have no clue that the link actually did its job.

This is what is broken about all current single page apps out of the box.


Plugging the hole

Thankfully most of the SPA frameworks and libraries provide a way to fill in this router gap. There are two different ways you could try and address this problem. Once the route has loaded either, announce the new page title to the screen reader or set focus on the new content once it has rendered.

Here are some examples on fixing the router in their respective framework / library:

React (react-router)

If you’re using React Router, you can set focus on new content by wrapping the Route component and utilizing componentDidMount:

Credit to Ryan Florence

Ember

In Ember, it’s really easy to plug the gap. As with everything in the Ember community, there’s an addon that takes care of this for you! ember-a11y will focus the newly rendered content for you. To install the addon you need to do two things:

  • ember install ember-a11y
  • Change {{outlet}} in your templates to {{focusing-outlet}}

That’s it!

Angular

In Angular (who arguably has the best a11y support, thanks Marcy!!), you can install the ngA11y package which provides two ways to fill the router gap:

I recommend checking out the ngA11y package documentation for all of the neat a11y related helpers it provides.

Vue

Vue is one of the libraries that doesn’t seem to have a solution that exists in the community. I could be wrong! But from my googling around I can’t find anything that will be easy for you to drop in.

I decided to provide a naive example for announcing a named route in the VueRouter:

Vue.js route announcer example

We name our routes ({ ... name: 'Foo Page' }) and use VueRouter’s afterEach callback to announce that the new page has loaded:

router.afterEach((to) => { 
if (!to.name) { return }
announcer.innerHTML = `${to.name} has loaded`;
});

Of course, this is a really contrived example. Hopefully, you won’t be setting the innerHTML of an element in your apps!


Why isn’t this done by default?

Seeing how it’s mostly pretty easy to plug this gap it begs the question: why isn’t this done from the start? I’m sure there are valid reasons as to why this isn’t solved out of the box, but I want to hear from the library / framework authors.

Usually developers working within these frameworks aren’t aware that this is a problem, which is okay! But this means that almost all single page apps go to production with this bug.

I’m not here to place blame or shame! My ultimate goal is to work with everyone to solve this problem from the start. Developers shouldn’t have to worry about this kind of thing — they should be building their application. I want to see what we can do as a community to help fix this issue that currently plagues single page apps.

Lets open this discussion up!