Read Accessible Vue

Chapter 5: Convey changes of state to screen-readers

"Gone are the days where users clicked on a link or button and web page had to be reloaded to show the updated content." Anil Kumar

Live regions

In a purely visual context, notifying your web app's users of changes in the web app's state is more or less a no-brainer. They click a button and usually see something appearing, changing or animating: a modal window is opened, a counter badge is updated, or a to do list item disappears. If the change is much more abstract, developers can solve this with a specialized notification system like humane.js or toastr. "Just" throw a new toast message (as they are sometimes called) to notify your users that something particular happened.

For users that consume your web app in a non-visual way, the topic of announcements becomes harder. Let's take a screen reader for example - its normal mode is to read the document from top to bottom (in reality, no user uses it this way, but navigates via headline structure, landmarks, links, controls, and the like). Given a screen reader user interacts with a button and, as a consequence, something happens in a web app, and this something is either obvious for visual users or accompanied by a notification. The problem is: without an accessible notification system in place said user has to actively search for what has changed on the website.

Best practice

But web developers have tools to send notifications for screen reader users - in the form of so-called ARIA live regions. As MDN web docs puts it:

Using JavaScript, it is possible to dynamically change parts of a page without requiring the entire page to reload — for instance, to update a list of search results on the fly, or to display a discreet alert or notification which does not require user interaction. While these changes are usually visually apparent to users who can see the page, they may not be obvious to users of assistive technologies. ARIA live regions fill this gap and provide a way to programmatically expose dynamic content changes in a way that can be announced by assistive technologies.

In a nutshell: If you add the aria-live attribute, an element (visible or not) it will become a "notification container". Screen readers recognize live regions, observe them and read out their text content as soon as it changes. Therefore, you can use this announcement tool to provide hints that are otherwise not perceivable to non-screen reader users. A simple example in four steps:

  1. Once your app mounts, or your page loads, you make sure that an element like the following exists (more on the values of the aria-live attribute below):

    <div id="info" aria-live="polite"></div>
    
  2. Now, let's imagine you have an online store that has a shopping cart, and you have an "Add to cart" button attached to each of your products. On click, a product will be added to the shopping cart. This would be perceivable for sighted users, for example with an animation.

  3. Your click handler for the aforementioned "Add to cart" button could look like this (just for the example only one button is present):

    document.querySelector('.add-to-cart').addEventListener('click', function() {
    // Your cart-adding business logic here
    // ...
    
    // Announcing success with aria-live
    document.getElementById('info').textContent = 'Product was added successfully to your shopping cart';
    });
    
  4. Then, when a product gets added, this is not only visually perceivable but will be also announced for screen readers. Usually, and using aria-live="polite", the announcement will wait until the screen reader has finished its current output. In the rare cases you need to report a critical error and cannot wait (read: cannot be polite), use aria-live="assertive". As soon as your live regions' content changes (from empty to not empty, in this case) it will be announced. A little tip to differentiate "polite" and "assertive": if you are, like me, not a native English speaker and use the word "assertive" rarely in your everyday English - just look at its first three letters; then you know you got the counterpart to "polite" ;-).

As for the usage of live regions in Vue, the script "Vue Announcer" offers a way to conveniently use these live region in your application.

You can install it with npm, as usual. At the time of writing this, the Vue 2 version of the script can be installed as follows:

npm install -S @vue-a11y/announcer

Whereas the Vue 3 version is on the "next" branch:

npm install -S @vue-a11y/announcer@next

Depending on the version, Vue Announcer must be registered in your main.js as a global plugin. For Vue 2:

import Vue from 'vue';
import VueAnnouncer from '@vue-a11y/announcer';

Vue.use(VueAnnouncer);

For Vue 3, the code would be as follows:

import { createApp } from 'vue'
import App from './App.vue'
import VueAnnouncer from '@vue-a11y/announcer'

createApp(App)
  .use(VueAnnouncer)
  .mount('#app')

The usage part consists of two steps. First, you have to add <VueAnnouncer /> to the template of (for example) your App.vue, so that the actual live region is registered for screen readers. To reiterate: That means that they observe its content and read it out once it changes.

<template>
  <div>
    <VueAnnouncer /> <!-- You can place it anywhere in your application. But you MUST add this custom element in order for vue-announcer to work -->
    ...
  </div>
</template>

For both Vue 2 and Vue 3 you are now able to send actual messages to this live regions by using this.$announcer.polite('My message') or this.$announcer.assertive('My urgent message'). Remember: live regions should improve the screen reader accessibility by providing status messages that are otherwise only conveyed visually.

For polite messages, let's come back to the e-commerce example from earlier. The place in your code where the data layer confirms that the item was successfully added to the card would be a suitable place for adding code like this.$announcer.polite('Added item to your shopping cart'). And the same goes for assertive announcements - which you should only use for really important messages. When your app detects that something went wrong, and the user urgently needs to be notified of that – this is a good place for an assertive message, like: this.$announcer.assertive('Could not autosave your document. Please save manually').

Accessible routing

Problem

Routing is an integral part of a Single Page Application (SPA). But what is "routing" in the first place? A SPA consists of one single HTML document (hence the name) - anything else is being loaded in an asynchronous way without ever really navigating off of the page. Changes within a page or between pages are "injected" onto the page. Quote Wikipedia:

(A Single Page application) interacts with the user by dynamically rewriting the current page rather than loading entire new pages from a server. This approach avoids interruption of the user experience between successive pages, making the application behave more like a desktop application.

So SPAs are asynchronous constructs - therefore all the strategies regarding notifying user of client side changes apply. However, there is one more peculiarity: "Routes" are not new pages in the sense that no new document is loaded – rather only necessary parts of the page (e.g. the main content region) are refreshed, "behind the scenes". Continue quote:

The page does not reload at any point in the process, nor does control transfer to another page, although the location hash or the HTML5 History API can be used to provide the perception and navigability of separate logical pages in the application.

That means Single Page Applications emulate changes of location by modifying the browser's location hash and by only changing parts of the page, asynchronously. This concept is called "routing",and it has accessibility issues.

Imagine a user interacting with your app in a non-visual way. Changing routes may be obvious to those who are visually abled - but they are hard to perceive for those who are not and use a screen reader. Its normal mode of operation is to read the document from top to bottom. While no screen reader user just lets the software read out the whole page in one sitting, they traverse the DOM. It is therefore not ideal when a screen reader user interacts with a link that leads to another route, and the screen reader stays silent. The route transition has worked, but the screen reader's virtual cursor (the "index finger" from earlier) stays on the activated link like nothing has happened at all. As a consequence, its user would have to actively search for what has changed on the website.

But screen reader users are not the only user group effected by client-side routing. Visual impairments are not binary, it's not "perfect vision" on the one hand, or "total blindness" on the other. Visual disabilities are on a spectrum. A fact that is not yet present enough in the minds of web developers is that not everyone uses your web app at the 100% zoom level. Some of your users enlarge the font sizes and expect your designs not to break (see basics mentioned and linked to in chapter 1). Some of them increase the general page zoom and by that activate media queries which where originally meant for smaller viewports. Some people view certain parts of your website in a magnified state (meaning: zoomed into parts, leading to an enlarged part of your website, but also many parts that are not visible in this very moment). The latter approach could be an issue with routing. Even when the focus is programmatically changed, the element gaining focus after the route change could be either outside of the viewport or so large that the focus state indicators aren't visible at all. To dig deeper into this challenge, please visit Marcy Sutton's research of this aspect of accessible routing.

A solution that is not yet worthy to call best practice

Since the concept of client-side routing is rather new, and since unfortunately accessibility does not yet play a major role in the JavaScript-framework communities (yet), there is no silver bullet for "solving" accessible routing yet.

One or two approaches are floating around, but none of them can be called "best practice" with a good conscience (yet). Rather, accessibility researchers are still at the beginning of a process of finding a best practice. A solution which you would "recommendable" doesn't only solve routing problems for one user group. It will be a strategy that works best for a maximum of people, and a maximum of disabilities. While the current best practices center on screen reader usage and client-side routing, other user groups are still confrontend with problems when using Single Page Applications.

Fortunately, research is happening in this field. Before I paraphrase the conclusions of Marcy Sutton's work for the GatsbyJS project let's look at the tools at our disposal:

Announcement/live regions

As introduced above, this tool helps us make (oftentimes invisible) announcements to screen readers. So an approach could be to fire a live region announcement after every route change, for example by programmatically hooking into the route change event. The announcement after the successful transition to the "About us" route could be "Current page: About us". However, implenting this strategy improves the experience for screen reader users only.

As you learned in chapter 4, skip links enable users to quickly reach internal parts of the page by the means of internal linking. This concept alone is not yet useful to improve the accessibility of client side routing, but in combination with the next tool in our toolbox, we're possibly one step further:

Focus management

To manage focus programmatically is, as of right now, the most common advise in context of accessible routing. We could hook into the moment of a successful route change and then set the focus to a part of the newly loaded content, a container or a headline.

Let's demonstrate how to ensure that focus is set to the wrapper after route transition. Imagine having your central App.vue file open and you are using vue-router. Register a watcher for route changes all across your app:

<script>
export default {
      $route: function() {
      // $nextTick = DOM updated, route successfully transitioned
      this.$nextTick(function() {
        // this.$el is in this case div#app
        this.$el.focus();
      });
    }
}
</script>

As the inline comment says: this.$el is in this scenario the element, where you mounted your Vue instance into, so probably a <div>. A <div> is not a natively focusable element, though. So it would have to be made programmatically focusable first. In concrete terms this means adding tabindex="-1" to it:

<template>
    <div id="app" tabindex="-1">
        <header>Header</header>
        <nav>Nav Items</nav>
        <main id="main">Content</main>
        <footer>Footer</footer>
    </div>
</template>

Combination

As you can see, every single tool listed above has its perks. Relying on a single tool alone leaves a user group uncovered. That is why the eventual preliminary recommendation of Marcy Sutton's research is as follows:

  1. Establish a skip link to your main content area (something you should do even when not dealing with client side routing). Put that skip link in visual proximity to the main content, if possible.
  2. After successful route change, programmatically put focus on the skip link (for how to that, see Chapter 2, "Keyboard Focus with $refs").
  3. Use an aria-live region to announce the route change to screen readers (for how to do that, see the "Live regions" section earlier in this chapter).

This way, screen reader users, folks relying on the keyboard as their single input device, and people employing screen magnification measures have an improved experience in your app. To see this approach in action implemented in Vue 2, head over to CodeSandBox for two little prototypes: Vue 2 and Vue 3.

My conclusion

While I think that the results of Gatsby's research offer a solid takeaway that considers many user groups and is therefore better that the heretofore "established" approaches, I fear that the developer's will have a hard time to sell a skip link becoming visible on every route change to their team and especially to their stakeholders. At the same time: if you are working on your own, and you are your own design department and boss, please do proceed and implement Gatsby's research results in your Single Page Apps.

To end this section with the central question: is Marcy Suttons's research recommendation the best practice we're looking for? I'm not yet decided. I reckon a lot more research with a lot more diverse users needs to be conducted, to even get near a solid best practice. Until then, it looks like an "it depends" situation (which are so common in web accessibility). It depends on your particular web app, and thus the need for user testing has to be even more emphasised that usual. So ask your users and conduct testing that is individual to your project. Yet, the important thing to remember is: Keep in mind that client-side routing is not only a challenge for screen reader users. Keyboard-only and screen magnification users are also affected from not having a "proper" page reload. And: Use all the tools at your disposal (focus management, live regions and skip links).

At this point in time, no super strategy has appeared that solves all problems for all users groups at the same time without forgetting someone else. But, at least, deploying one (or all) of the strategies listed above is better than doing nothing at all. Currently, accessible routing is far from being a fully explored topic. So, if you do research in this realm, please do share it!

Action Steps for this chapter

Go to the next chapter ›

Last update 2021. Creative Commons BY-NC 4.0