Angular Router and Bypass Blocks : a surprising accessibility challenge

Here’s the tale of how fixing what was supposed to be an easy-to-fix accessibility issue turned out to be a challenge thanks to Angular and how I overcame it… Sort of!

What are bypass blocks?

If you’ve looked into web accessibility before there’s a good chance you’ve heard about bypass blocks before, maybe called skip links. They are links, at the very beginning of an HTML page (often hidden unless focused) that enables some users to navigate pages faster by jumping directly to specific parts of the page, most often the main content of the page or the navigation.

For users who’s main way of browsing a page is through tabulation, it can be very tedious to have to start at the beginning of the page every single time and go through the header with its logo and navigation, the breadcrumb, the sidebar and so on. Having a link that jumps you right where you need to be can be a huge timesaver !

It’s also a very well known technique because it’s often very easy to setup : it’s just an anchor tag that points to a specific ID in your page, like so :

<a href="#main">Skip to content</a>

You only need to have the same ID on the main part of your page everywhere, but with templating systems used in most frameworks and CMSes, this is usually quite easy !

Being easy to do and very useful for users who need it is what makes this such a common thing to address when it comes to accessibility. Except when it becomes difficult…

What’s Angular Router?

I assume you know Angular. Angular Router is one of its core plugins, that is used to handle the routing inside the app : having the right portion of code being executed based on the URL you are browsing. In all Single Page Applications this is done at two different times : when the page loads for the first time and the URL is a link you clicked on, or that you had bookmarked for example, which may or may not be the index route of the application, and when you navigate inside the app clicking its buttons and links.

To support this second features different frameworks and plugins work in different ways and it just so happens that Angular Router overrides how all clicks on links are handled by the browser, to prevent a page refresh. Makes sense… Except for anchor links, which skip links are…

And this is where things go bad : if you use the skip link I wrote earlier, Angular Router will interpret it as such :

<a href="/#main">Skip to content</a>

It’s only a / difference, but it redirects you every time to the index route of your application, which is really not what you want!

Known issue, known solutions

Because this is such a common accessibility feature, of course other people ran into this issue and found solutions for it. The best one I could find is this one : A Quick Note on Skip-Links in Angular by Stephen Belyea from 2018. It’s been updated a few times since then and basically requires you to make the link a lot more complex than it needs to be to get the route URL from Angular Router to which you append the ID of your main content. It really should not be this complex to get this to work.

Except that didn’t work for me. I wish it had but unfortunately this doesn’t work if you have specific parameters in your route that Angular Router doesn’t care about, for example with ?key=value style like we did. Those would not be available through Angular Router’s API and couldn’t be added to the link’s href.

The alternative I found

If Stephen’s solution works for you, that’s great and this is what you should use, but if for some reason it doesn’t, then here’s the solution I’ve found :

  • Use a <button> instead of a <a>
  • Use a (click) event
  • Move the user’s focus using document.querySelector('#main').focus()

This also requires that I add tabindex="-1" on my #main element otherwise you can’t programmatically focus it.

<button class="skip-link" (click)="skipToContent()">
  Skip to content
</button>
public skipToContent(): void {
  document.querySelector<HTMLElement>('#main').focus();
}
<main id="main" role="main" tabindex="-1">
  ...
</main>

This solution has drawbacks that all come from the fact that it is not a real link :

  • It won’t appear in the page’s list of links
  • Wheel-click to open in new tab won’t work
  • The browser won’t display the target URL
  • The browser won’t change the page’s URL to show the anchor

For all these reasons I figured I’d try to make it a real link and override the click event. This turned out to be even worse :

  • Wheel-click worked but redirected to the index route (again)
  • The browser would still not display the right URL with the anchor

For these reasons I figured, despite its drawbacks, that the button solution was the more robust one, after all a button moving focus is something that is normal in web apps, that works and that is compliant with the specs, and it required less code, which means less chance of something breaking in the future.

Conclusion

I find it very surprising that an accessibility topic such as this one would require such complex circumvolutions with Angular and it shows how much work still needs to be done to push accessibility as a real important topic in the web.

This might be the warning sign for more issues to come in my journey to making an Angular app accessible, if that’s the case I’ll have to write a new blog post about it. Let’s hope it doesn’t turn into a series…