Developers face pretty specific challenges when creating a highly accessible single page application (SPA) user experience. This in-depth tutorial outlines some best practices and accessibility challenges common to SPAs (and specifically Angular apps) so you can build an SPA that works for everyone.

How to make usable Angular.js projects

We’ve been seeing a lot of single page applications (SPA) in our assessments and projects lately. We’ve also noticed some pretty specific challenges for developers trying to create a highly accessible SPA user experience. When we see patterns like this, we’ll often put an example together for our clients to illustrate how various design and accessibility issues could be solved. We also created a simple CRUD (Create, Read, Update, Delete) demo app using Angular.js connected to a Firebase backend to illustrate some accessibility best practices. We thought we’d share it with you, and run through some of the accessibility challenges it addresses. Of course, what better app to share than your very own Accessibility Ticket Tracker Application (source on GitHub).

Screen shot of Accessibility Ticket Tracker Application

What you’ll learn

Before we get started, you need to know… this tutorial isn’t designed to be “everything you’ll ever need to know about making Angular.js apps accessible.” This is a walk-through of some fairly common accessibility challenges to watch out for when building SPAs, and how you can address them when using Angular.js.

Here’s what we’re looking at:

What you’ll need

The goal of this post is to outline some best practices and basic-to-intermediate accessibility challenges common to SPAs (and specifically Angular apps) that developers should be on top of.

Note: This post is very Angular specific and you should have a basic understanding of how Angular works to follow the examples. That being said, many of the principles apply to all applications, whether it’s an SPA or not.

Document structure, navigation and keyboard accessibility

Document structure

When looking at any application for accessibility concerns, we always start with document structure and keyboard navigation because of one simple fact: if you can’t get to it, you can’t use it. Often, the first challenge that crops up with navigation and SPAs is a semantic document structure, which includes proper heading levels.

Many users of assistive technologies, like screen readers, rely on the document structure to move between the important parts of the document or application. This includes things like jumping between headings.

Benefits of document structure

  • Starting with a solid, well-structured webpage enables keyboard access to go well beyond just the tab and arrow keys.
  • A good document structure with proper heading levels also provides great anchor points to shift focus to when moving between different application views. (We’ll touch on that later.)

Code Drop: document structure

Here’s the bare-bones document structure for our application:


<!DOCTYPE HTML>
<html ng-app="a11yTicketApp" lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Accessibility Ticket Tracker</title>
  </head>
  <body>
    <div class="container">
      <header class="header">
        <nav>
          ...
        </nav>
      </header>
      <main class="main" id="main">
        ...
        <h1>Accessibility Ticket Tracker</h1>
        ...
        <h2>Tickets</h2>
        ...
      </main>
      <footer class="footer">
        <p>Copyright</p>
      </footer>
    </div>
  </body>
</html>

Page and view titles

There’s also another commonly forgotten part of the document structure that helps with navigation: the page title. It’s natural to think “single page application, single page title,” right? But when we are building SPAs, we’re changing from view to view. These views should be interpreted as changes in web application pages. So, just like any other web application, the page title should change with each new view, keeping people in the loop about where they currently are in the application at all times. Angular has a terrific routing system that makes updating page titles really easy. Let’s take a look.

Code drop: Angular routing and page titles

Web browser tab with title showing Accessible Ticket Tracker

The first step in setting up page titles for views in Angular is to bind the root title property to the title tag of the default index.html document. To set binding in the head of the html document, you’ll need to have the application declared on the html tag so you can access the $rootScope variables used in Angular’s $routeProvider. For example:


<html  class="no-js" ng-app="a11yTicketApp" lang="en">
<title ng-bind="title">Accessibility Ticket Tracker</title>

Where you’ve defined your application routes, you’ll need to add in a title property to your routes.


.config(function ($routeProvider) {
    $routeProvider
        .when('/main', {
            templateUrl: 'views/main.html',
            controller: 'MainCtrl',
            title: 'Accessibility Ticket Tracker'
        })
        .when('/edit/:id', {
            templateUrl: 'views/edit.html',
            controller: 'EditCtrl',
            title: 'Edit Ticket'
        })
        ...  
        .otherwise({
            redirectTo: '/main'
        });
})

Now we listen for page route changes using the $on('$routeChangeSuccess') event, and update the $rootScope title attribute.


myApp.run(['$location', '$rootScope', function($location, $rootScope) {
    $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
        // test for current route
        if(current.$$route) {
            // Set current page title 
            $rootScope.title = current.$$route.title;
        }
    });
}]);

Keyboard navigation

Now that we’ve got document structure under our belts, let’s look at one of the things we all love about Angular: the ability to use ng-click to wire up all our interfaces. This directive provides awesome power and convenience when building applications, but that’s also one of its greatest dangers on the accessibility front, because you can add ng-click to any element. You’ll often find div tags and other elements with click events added to them. Unfortunately, these elements don’t receive native focus by the browser, which creates a barrier for keyboard users. This is where creating interfaces that follow web standards and best practices is critical in development. Use buttons where an action is associated with an navigation element (e.g. submit form), and links when moving between locations in the application.

Let’s see some examples from our Angular app.

Code drop: buttons and links

First, here’s the approach we’re seeing a lot lately, where divs are used for clickable elements when a native element should be used:


<div ng-click="doSomething(id);" class="im-a-button">I'm  not a button</div>

Here’s a sample from our application where we use a button for our clickable interface items. Sticking to the native elements means you get all their accessibility goodness.


<input type="button" value="Cancel" class="btn btn-primary" ng-click="go('main')">

Now let’s take a look at how we set up our links. This link reveals record details so the ng-click overrides the href attribute. However, we still keep the href attribute there, so we can give each link a unique destination that will register with screen readers. We also use visually hidden text that screen readers have access to (that isn’t displayed) to convey the state of the link. Using Angular expressions makes enabling and disabling text extremely easy.


<a href="#expandDetails-{{ ticket.$id }}" ng-click="toggleTicketDetails($event,$index)">
  {{ ticket.summary  }}
  <span class="visuallyhidden">{{ index==$index ? " - click to hide details" : " - click to show details"}}</span>
</a>

CSS


.visuallyhidden {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  position: absolute !important;
}

Note: This is visually hidden text, not hidden text, so we’re not using CSS display:none.

The last step for keyboard navigation is to look for ways to save folks from using excess keystrokes. You can see this demonstrated in our Ticket Tracker application, where we save the search/filter variable of the main ticket list so that people aren’t required to retype the search each time they return to the main ticket list. Check out how we set that up in Angular.

Code drop: storing search queries

Text input filtering table list search results

We bind our search model to our ticket search input.


<input type="text" id="ticket-filter" ng-model="search" class="search-query" placeholder="Search">

Now we’re able to filter and search our tickets by using Angular’s built in filters to limit what’s displayed by the ng-repeat directive.


<tr ng-repeat="(id, ticket) in tickets | filter:search" id="{{ticket.$id}}" set-last-ticket-id-focus>
  ...
</tr>

When people move to a new screen, say to edit a record, they’ll want their search to be persistent when they come back, so we need to store their query string for later. For now, we can just add a variable to $routeScope that can be accessed between controllers.


.run(['$location', '$rootScope', function($location, $rootScope) {
  ...
  $rootScope.search = "";
  ...
})

When the main controller that holds our ticket list is loaded, MainCtrl, we can set its search model to the root search property and repopulate the search input element.


.controller('MainCtrl', function ($scope, $rootScope, Tickets, Flash) {
    $scope.search = $rootScope.search;
});

But, there’s one missing piece. How do we update the root search property when the user updates the search input field? Use Angular’s $watch() method from within our MainCtrl controller which, in this case, watches for updates to the value of the search model and updates our root level search property. Voila:


// Watch when search field is updated and update global search variable
$scope.$watch("search", function(newValue, oldValue) {
    $rootScope.search = $scope.search;
});

With the base of a well-formed document, including views with unique page titles and keyboard-accessible navigation elements, our SPA is in good shape. In the next sections, we’ll take a look at techniques to take our app’s accessibility to the next level.

Focus management

One of the unique features of single page applications that can create challenges for people using screen readers is that there’s never a page refresh, only view refreshes. As a result, the focused element often disappears from the interface, and the person using the screen reader is left searching for clues as to what happened and what’s now showing in the application view. Places where focus is commonly lost include: page changes, item deleting, modal closing, and expanding and closing record details.

As web developers, we have some strategies that can manage focus for our audience so they are never lost:

  1. The use of JavaScript to set focus to form elements and links.
  2. The ability to give any element a tabindex of -1 or 0, which will then also enable JavaScript to set focus to it.

Let’s see how we can use these strategies to help people stay focused within applications.

Code drop: h1 focus on page changes

Web page with focus set to h1 level heading Create a ticket

We’ve already looked at Angular’s page routing, so let’s add to that example and help people using keyboards stay focused in our application by setting focus to the h1 element as we move between pages. This informs people using screen readers where they are, and saves them having to click through the navigation again.

Again in the example below, we get our current route, but we also have to check for a previous route. If this is the first time the page loads, we don’t want to skip the header content and set focus to the h1. We do all this using Angular’s $rootScope.$on('$routeChangeSuccess') method. We’ve also added variables to hold the history and current URL paths to check later.


myApp.run(['$location', '$rootScope', function($location, $rootScope) {
    ...
    
    var history; // stores uri of last page viewed - Used to track if we should set focus to main H1
    var currentURL; // store uri of current page viewed - Used to track if we should set focus to main H1

    $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {

        // Test for current route
        if(current.$$route) {
            // store current path
            currentURL = current.$$route.originalPath;
            // set page title to current route record
            $rootScope.title = current.$$route.title;
        }

        // When navigating between pages track the last page we were on
        // to know if we should be setting focus on the h1 on view update
        if(previous) {
            if(previous.$$route){
                history = previous.$$route.originalPath;
            }
        }
    });
}]);

Here’s where we need to add a second method to our run block. With Angular, there’s a delay between when the route is set and when the view is actually rendered. If we try to set focus before the view is rendered, focus will be lost again, defeating the purpose. For brevity, we removed the details of the $on('$routeChangeSuccess') method above and instead added a $rootScope.$on(‘$viewContentLoaded'), which is called once the view has rendered. Now, we can set the focus to the page’s h1 heading. Of course, we only do this if there is a value to the history variable, otherwise it’s our first page visit on the site and we don’t set a focus.

Now we use JavaScript to first set the tabindex to -1 on the h1 heading element and then set focus to it. This can be done using some jQuery.


$('h1').attr("tabIndex", -1)
$('h1').focus();

Or if we chain it, like this:


$('h1').attr("tabIndex", -1).focus();

So, why do we choose a tabindex value of -1? The -1 indicates that only JavaScript can set focus to that element. If we used a tabindex value of 0, it would indicate to the browsers that we want that tabindex inserted into the document tab order list. By using -1, we’re able to set the focus to the element. Once the person leaves, she will not be able to tab back to it, so in this case, the h1 keeps its native tab-ability.



myApp.run(['$location', '$rootScope', function($location, $rootScope) {
    var history; // stores uri of last page viewed - Used to track if we should set focus to main h1
    var currentURL; // store uri of current page viewed - Used to track if we should set focus to main h1

    $rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
		...
    });
    $rootScope.$on('$viewContentLoaded', function () {

        // Once the template loads set focus to the h1 to manage focus
        // if there is no history do not adjust focus this is the first page the user is seeing
        if(history) {
            // Default - set page focus to h1
            $('h1').attr("tabIndex", -1).focus();
        }
    });
}]);

Now that we get the principle, let’s review some other places in our application where we’ve used this. The following code drops outline some common scenarios where focus is often lost.

Code drop: cancel on ticket edit

Search row with edit button with focus indicator on it

Setting focus isn’t always so straightforward. Lets look at an example in our app where the person decides to edit a ticket, but then clicks cancel. In this situation we want to return the person back to the edit button which she clicked. Now we need to start tracking the latest ticket ID viewed for editing.

We start by creating a property on the $rootScope that we can use to track the last ticket ID we opened for editing across controllers.


// track last ticked opened to edit - used to return focus to list edit btn
.run(['$location', '$rootScope', function($location, $rootScope) {
  ...

  $rootScope.lastTicketID = "";

  ...
})

Once we enter edit view, we record the ID of the ticket we are viewing. Later, we can use it to set focus back to its edit button in the main ticket list if the person clicks cancel.

In the controller, we create a “go” method that binds to the cancel button and sends people back to the main ticket list. This function simply sends people back to the view passed to the function.


.controller('EditCtrl', function ($scope, $rootScope ,$location, $routeParams, $firebase, fbURL, WcagscService, SeverityLevelService, Flash) {
    // Add record to scope -- ticket object is coming from Firebase DB
    $scope.tickets = ticketsObject;
    ...

    // track record id so that we can set focus back to edit button if user hits cancel
    $rootScope.lastTicketID = $scope.tickets.$id;

    ...
    // Cancel button function
    $scope.go = function (path) {
        // indicate last form viewed
        $rootScope.lastForm = "edit";
        // send user to path provided in ng-click
        $location.path(path);
    };
})

Now we hit a snag with Angular because of its delay rendering. First, the view renders. Then, there’s another delay while the ng-repeat renders. So if we try to set focus back to the button on the $rootScope.$on('$viewContentLoaded') like in the example above, we have a problem, because the data table hasn’t rendered yet.

We need to make sure our table data has rendered. To do this, we add a directive to our ng-repeat element which we’ll call setLastTicketIdFocus.

First add the directive to the ng-repeat.


<tr ng-repeat="(id, ticket) in tickets | orderBy:['firstName'] | filter:search" id="{{ticket.$id}}" set-last-ticket-id-focus>
  ...
</tr>

Now, we create a directive using the $last property to check if we’ve iterated to the last element in the ng-repeat. We also make sure it has rendered, so we add a $timeout() service call giving the element time to render. Once we know we’ve got our list, we can set focus back to the edit button for the record we were last on by using this jQuery line: $("#" + $rootScope.lastTicketID + " .edit-btn").focus();. This will set focus to the edit button within the table row with the ID of our ticket database ID.


.directive('setLastTicketIdFocus', function($timeout, $rootScope) {
    return {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true && $rootScope.lastTicketID != "") {
                $timeout(function () {
                    if($rootScope.flashMsg == "" && $rootScope.lastForm == "edit"){
                        $("#" + $rootScope.lastTicketID + " .edit-btn").focus();
                        $rootScope.lastTicketID = "";
                        $rootScope.lastForm = "";
                    }
                });
            }
        }
    }
})

Note: Check to make sure there are no flash messages showing—something we’ll explore in the next example.

 

Code drop: success messages

Record saved flash message with focus indicator on it

Part of keeping your audience in the loop involves making sure that they’re aware of every notification displayed on the screen. Flash messages are a very common way to do this in SPAs, but how do we make sure that everyone is aware of these messages? Again: set the focus on notifications once they are displayed. To do this, we need to set up a factory to track flash messages across views. This factory gives us a mechanism to let us set, get, and clear messages.


// Store and and queue flash messages used on save and create
.factory("Flash", function($rootScope) {
    var queue = [];
    var currentMessage = "";
    $rootScope.flashMsg = currentMessage;

    $rootScope.$on("$viewContentLoaded", function() {
        currentMessage = queue.shift() || "";
    });

    return {
        setMessage: function(message) {
            queue.push(message);
            $rootScope.flashMsg = message
        },
        getMessage: function() {
            return currentMessage;
        },
        clearMessage: function($event) {
            currentMessage = "";
            $rootScope.flashMsg = "";
            $event.preventDefault();
            $('h1').attr("tabIndex",-1).focus();
        }
    };
})

Now that we have our flash factory set up, we can set messages here when we have a successful edit update or created records. To do this on a record edit, we start by adding a line in our edit controller that clears out any previously stored flash variables. This is so they don’t trigger a message display, in case we go back to the main view before we save our record. Once the form is successfully submitted and the information is in the database, we can then set a new flash message using our setter Flash.setMessage("Record saved!").



.controller('EditCtrl', function ($scope, $rootScope ,$location, $routeParams, $firebase, fbURL, WcagscService, SeverityLevelService, Flash) {
	...

    // Clear any flash variables we have stored
    Flash.setMessage("");

    // Handle form submits (with errors)
    $scope.edit = function (inValid) { // invalid is passed from angulars form processing
        if (inValid) {
			...
        } else {
            // If form has no errors - save results to database
            var edit = $scope.tickets.$save();

            // check that submit was successful
            if (edit) {
                // if submit was successful
                // set flash message to be displayed
                Flash.setMessage("Record saved!");
                // send user to main screen
                $location.path("#/main");
            } else {
                // if not successful warn user
                alert('something went wrong');
            }
        }
    }
})

In our main controller, we include the flash factory and make the flash message property available to our view.


.controller('MainCtrl', function ($scope, $rootScope, search, Tickets, Flash,lastTicketIdTracker) {
    $scope.flash = Flash;
	...
})

Next, we check in the view using ng-show to see if there is a flash message. Important: to hide the message, all you have to do is clear the value of the flash message using flash.clearMessage() and Angular’s ng-show will make it disappear. Sweet! Hold on tight, though, because we’re going to lose focus again, so we need to manage focus on the clearMessage method, too. We use our well-structured document with its great anchor points so we can set focus to the h1 tag to let all users know where they are.

The “x” we click to close the message is called a glyphicon, which is okay for sighted people, but leaves non-visual folks guessing why the flash message is a link. This is where our handy visually hidden text clarifies the action of the link. We also use aria-hidden="true" on the glyphicon span tag to hide it from assistive technologies. This prevents some screen readers from announcing an interpretation of the Unicode character used for the graphic icon.


<div id="flash-message" class="alert alert-success" ng-show="flash.getMessage()">
  <p>
    <a href="#clearAlertMessages" ng-click="flash.clearMessage($event);">
      <strong>Success:</strong> {{flash.getMessage()}}
      <span class="glyphicon glyphicon-remove" style="float:right"  aria-hidden="true"></span>
      <span class="visuallyhidden">Click to hide message</span>
    </a>
  </p>
</div>


// Provide custom page titles on route change
// Set focus on view update
myApp.run(['$location', '$rootScope', function($location, $rootScope) {
	...
    $rootScope.$on('$viewContentLoaded', function () {

        if(history) {
			...
        }

        // If there is a flash message set focus to it - trumps all focus
        if($rootScope.flashMsg != ""){
            $('#flash-message a').attr("tabIndex", 0).focus();
        }

    });
}]);

 

Code drop: displaying ticket details

Ticket list item with description expanded and description heading with focus

When jQuery introduced itself to our world, we all got very creative in the ways we’d hide and show extra content on a page to make people’s interactions efficient and engaging. For sighted people, sliding open panels is generally clear and intuitive. On the other hand, non-visual people could miss sliding panels completely, unless they get a hint that something has been added to the screen. Focus management to the rescue.

In its simplest form, the basis of a show-and-hide panel is toggling an element’s CSS display property between block and none. Here’s how we show and hide ticket details in our interface:


	tr .ticket-details {
		display: none;
	}

	tr .showdetails .ticket-details {
		display: block;
	}

Angular has a great ng-class directive that lets us do this by toggling a class depending on the value of a variable. For example: ng-class="{showdetails:index==$index}". If the index variable matches the record $index, the showdetails class is added to the element. How do we set that index variable? We bind a click event to the ticket summary link that calls the toggleTicketDetails method in our MainCtrl controller.


<tr ng-repeat="(id, ticket) in tickets | orderBy:['firstName'] | filter:search" id="{{ticket.$id}}" set-last-ticket-id-focus>
  <th scope="row" ng-class="{showdetails:index==$index}" id="ticket-{{$index}}">
    <h3>
      <a href="#expandDetails-{{ ticket.$id }}" ng-click="toggleTicketDetails($event,$index)">
        <span class="glyphicon glyphicon-{{ index==$index ? 'minus' : 'plus' }}-sign"  aria-hidden="true"></span>
          {{ ticket.summary  }}
        <span class="visuallyhidden">{{ index==$index ? " - click to hide details" : " - click to show details"}}</span>
      </a>
    </h3>
    <div class="ticket-details">
      <h4>Description:</h4>
      <span>{{ticket.description}}</span>
      <h4>Recommended Fix:</h4>
      <span>{{ticket.fix}}</span>
    </div>
  </th>
  ...
</tr>

Now we can drive home the point of this example. Once we’ve passed in the current index of the ticket we want to see the details of, and the class has been added to the element through ng-class to make it visible, we need to set focus to the description heading of the ticket summary. Watch that rendering delay. We’ll wrap the focus statement in a $timeout() service call and then we can set focus to the first h4 of the ticket row with this line of code: $('#ticket-'+ index + " h4").first().attr("tabIndex",-1).focus();.


.controller('MainCtrl', function ($scope, $rootScope, search, Tickets, Flash, $timeout) {
 	
 	...

    // track toggle state of record details - sets scope index to current index
    // uses ng-class to display table row indexes
    $scope.toggleTicketDetails = function (e, index) {
        if ($scope.index == index) {
            delete $scope.index;
        } else {
            $scope.index = index;
        }
        e.preventDefault();

        // Add delay to allow time for element to render then set focus to header
        $timeout(function() {
            $('#ticket-'+ index + " h4").first().attr("tabIndex",-1).focus()
        });
    };

    ...

})

Focus management is an often forgotten part of accessibility, but for SPAs it’s essential. Think about how much can change in that single moment when focus is lost. In an instant, a person is navigating a completely different interface with potentially no idea what the purpose of the screen is. When your app uses similar focus management techniques to lead users through your application, you can trust that they can effectively achieve their goals.

Visually hidden text

We often see JavaScript frameworks and various UI frameworks use the title attribute to provide a text alternative for an interface element where the purpose of an interactive element is conveyed visually—perhaps with only an icon to provide meaning. Because assistive technologies are inconsistent about the way they support the title attribute, we recommend using visually hidden text that’s embedded in the tags of interface elements. Angular makes it easy to add in and adjust the text of visually hidden elements to provide a richer, more navigable interface for your audience.

Code drop: displaying ticket details continued

Let’s use our last example to help illustrate how we use visually hidden text to convey interface states. Check out the span with the class="visuallyhidden" below. Using Angular’s ability to resolve expressions right in the template, we can easily toggle whether we include the ” – click to hide details” or the “- click to show details” text in the link. People using screen readers are now clear as to whether or not the ticket details are showing.


<a href="#expandDetails-{{ ticket.$id }}" ng-click="toggleTicketDetails($event,$index)">
  <span class="glyphicon glyphicon-{{ index==$index ? 'minus' : 'plus' }}-sign"  aria-hidden="true"></span>
  {{ ticket.summary  }}
  <span class="visuallyhidden">{{ index==$index ? " - click to hide details" : " - click to show details"}}</span>
</a>

CSS


.visuallyhidden {
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  position: absolute !important;
}

 

Code drop: repeating links edit and delete

Two rows of search results with a highlight box around all the edit and delete icon links

Although we use glyphicons to provide an easy way to import and use icons throughout our interface design, they don’t provide information to people who use screen readers. A common solution to remedy this is to use visually hidden text to provide the text content of “Edit.” But our interface has a series of edit buttons. So in this case, Angular makes it really easy to just append the summary property of the ticket object to the edit button to make each edit link unique. Now our audience can quickly tab through the interface and have a full description of the element they are on and its action.



<a href="#/edit/{{ ticket.$id }}" class="edit-btn">
  <span class="glyphicon glyphicon-edit"  aria-hidden="true"></span>
  <span class="visuallyhidden">Edit {{ ticket.summary  }}</span>
</a>

Using visually hidden text is the most consistently supported method of providing extra information to UI elements for assistive technologies. Getting just the right text to convey the meaning of what’s presented visually is sometimes challenging, but it’s also a fun place to shape your application and users experience at another level.

ARIA live

Angular’s two-way binding of elements to a data source is mind blowing. It provides us the ability to update multiple areas of the interface at once. But, with the power of multiple element updates, comes the challenge of how to keep non-visual folks informed of all the updates. Let’s look at the example in our ticket tracker of how we use the aria-live attribute to keep our audience in the loop about interface updates.

Code drop: ticket search and multi-element data binding

Search tickets input with contrast entered and text below it reading showing 1 of 2

When we filter our search in the Ticket Tracker, we need to give our readers clues that things are changing on the screen. Sighted people will just see the changes. Well, that isn’t quite right: what about sighted people using screen magnifiers viewing the screen while zoomed in? These folks may not see any screen updates. On the other hand, individuals who are using screen reader software need another mechanism to be notified of interface updates. To address these issues, we use a line right below the input field that indicates the total number of records and how many are currently displayed. Using this approach, people using screen magnifier software will be able to see if their query is affecting the number of records displayed.

To notify people using screen readers, we use the aria-live attribute. The aria-live attribute will notify screen reader software any time the content within the containing tag is updated. The value of polite indicates it will announce the updates once the screen reader finishes a current task. It won’t cut off other important messaging. The aria-atomic="true" attribute indicates that the whole line should be read—not just the text sections that have changed. This way, we can keep people using screen readers in the loop of interface updates as well.

Take a look at a bit of the Angular helping us provide information to our readers. In this line, the first is {{tickets.length}} which outputs the number of total tickets in the database. Applying the snippet {{(tickets|filter:search).length}} is a neat trick to learn the number of records that our filter is returning. With those two pieces of information, we can fully convey the state of the search to our audience, similar to how we’d indicate “showing 6 of 12” in a list item.


<div role="status" id="ticket-table-info" aria-live="polite" aria-atomic="true">Showing {{(tickets|filter:search).length}} of {{tickets.length}}</div>

ARIA live is a great tool to use when trying to convey screen updates to assistive technologies, especially when shifting the tab focus away from the current UI would really interrupt the flow of someone trying to complete a task. Take a look at some of the interfaces you’ve built and see if there is a place where ARIA live could help keep your users more informed about what’s happening visually on screen.

Angular’s ngAria and ARIA in general

JavaScript and UI frameworks are pretty magical. It’s exciting to see the community rallying to make the process of creating accessible interfaces as simple as possible. Often, you’ll see ARIA being thrown into interfaces to fix problems when using native controls could fix these issues in a cleaner, more predictable way. We recommend starting native, and only use ARIA where it improves the experience for users—not to replicate existing functionality. For example, if you are adding ARIA using ngAria to describe whether checkboxes are checked or not, try asking yourself, “Why am I adding this? Checkboxes have these states available natively.” To clarify, we’re not saying stay away from the ngAria module, just understand why you’re using it, and what problem you’re attempting to solve.

Here’s some other good reads and resources for Angular Accessibility:

Summing it all up

Angular.js, and frameworks like it, are fantastic tools for rapidly creating great interactive applications, but they come with their own set of accessibility concerns.

Remember to use the following strategies to keep your single page application targeted:

  • Start with a focus on creating a completely keyboard accessible interface
  • Manage the focus throughout the entire application
  • Describe any ambiguous elements through visually hidden text and keep all users informed of changes in the interface through ARIA live notifications.

The key to taking your web application further is to make sure you’ve got a clear plan for how you’re going to implement these frameworks to enable the widest and most inclusive audience. With a little creativity, you’ve got it nailed. It takes all of us to build a web for everybody.