Accessible way of notifying a screen reader about loading the dynamic Web page update (AJAX)

The rise in the use of AJAX to dynamically update the website instead of using standard hyperlinks and separate Web pages has resulted in accessibility problems for users of assistive technology (e.g. screen readers).

I am inquiring here the public opinion of an accessible solution for the best possible user experience. Unfortunately the most relevant accessibility resources, for me at least, such as Google, ADG*, WebAIM, W3 - WCAG, MDN etc. did not provide the specific answer. The topic is not targeted to single page applications only. Many websites face similar issues when loading the dynamic content e.g. "Load more" button, tab content, modal content, pagination page change etc.

The issue

Dynamic content update instead of a standard Web page request?

  1. A screen reader user clicks the link and nothing happens "apparently".
    It creates a poor user experience as the link might appear broken. The expectation was opening a new page and positioning to the top of the content. A user is not aware that a part of the existing page is about to get updated soon. Screen readers do not announce content updates automatically (except in specific cases).

    Requirement #1: A user must be aware that a content loading has started, possibly with a delay e.g. after a request is taking longer than X seconds.

  2. A user focus remains on a link with no easy way of jumping to the updated content once the update was completed. There might be a bunch of other elements to skip over. Clicking the link a user expected to access the content straight away.

    Requirement #2: A user focus must move to the new content, preferably a heading element, after the update has completed.

The solution - Easy part

The requirement "A user focus must move to the new content" seems straightforward:

Use Javascript to move the focus to the heading element when AJAX request is completed!
In order to focus a heading, which is otherwise non-focusable element, it must have tabindex="-1" attribute.

The following resources answers "Why is it important to move the focus?", "How to do it?" and "Why is it better to focus a heading element instead of focusing the entire container?":

Managing focus and focusing the heading element: This is one of the rare cases where it's ok to remove the CSS outline (on heading). — Youtube - Rob Dodson from A11ycasts

Two solutions to help non-sighted or keyboard-only users get to where they're going:
Emulate a native page load and move focus to the top of the document.
Move Focus on the content (heading or possibly form control). — daverupert.com - Accessible Page Navigation in SPAs

The most reliable way I found to overcome this challenge was to set the focus on the heading of the area that's changing. — Marwan Aziz @medium.com - Accessibility considerations when building SPA

If the focus is set to an element, the screen reader announces it. This can be an easy way to inform users about something on the page: simply set the focus on it. Be sure though that this does not result in disorientation for the screen reader user: it shouldn't happen "out of the blue", but only when the screen reader user has done some interaction that caused the page change.
And by the way, you should not set the focus on a big container of information. — accessibility-developer-guide.com

The developer can simply allow the update to occur and not inform the user of it, alert the user of the update through some sort of embedded audio sound, or can set focus directly to the updated content. — WebAIM - Dynamic Content Updates

For single page applications, browser focus should be set on the <h1> heading after a new view is loaded. — Yale University

The user's focus is directed to new content added to the page. — Google Chrome DevTools - Accessibility Audit - Additional items to manually check


What if loading takes too long? - The tricky part

The requirement was also "to inform a user that a content loading has started". It makes sense as the user might be wondering why nothing happens after clicking the link. How would a user know whether to wait or the link is simply broken?

Are there any opinions? Ideas?

What I tried so far?

  • BAD SOLUTION: Use a placeholder where the new content suppose to appear and move the focus there immediately. The placeholder contains an initial heading text "Please wait. Loading..." and it is being read immediately when focused. So far so good. But after testing with JAWS, NVDA and various browsers I decided against the solution because it simply wouldn't work as expected. It was somewhat buggy. Often after the new content update was completed the screen reader would be still reading "Please wait. Loading..." text and sometimes even the new content was not read at all even though I re-focused to the new updated heading again
  • BAD SOLUTION: Show a loading modal dialog if a request takes longer than X seconds. User would be informed that a loading modal appeared and the modal title would be "Please wait. Loading...". After the content update was completed the modal would be closed and the heading of the new content would be focused. This worked kind of OK but troubles the user experience. Opening the modal takes a while, especially when animated so it could happen that the modal opens after the content was already loaded making a confusion. It does not really seem to be user friendly. It interrupts the user by popping-up in the middle of the screen and hiding the current content. It also mismanage the focus for screen reader causing disorientation. Because focus must be moved to a modal. Simply, it's senseless showing the modal dialog with OK button just to inform a user about loading. Modals are usually used for the user to interact with

Lastly... ARIA live region - Not perfect but the best so far...

The latest idea was to implement a balloon notification message which becomes visible if an ongoing AJAX request takes longer than X seconds. It has an ARIA live region aria-live="polite", a loading icon and a text message "Please wait. Loading...". When the request is completed the message is changed to "Loading done." but hidden visually and remain visible only to a screen reader so it can announce it. I did not use role="status" due to lack of consistent and cross-browser support. The user focus would never move. It remains on a clicked element until the request was done but the user is still informed that loading is in progress or done and in a polite way. So far so good.

enter image description here

I tested the solution successfully on Windows 8.1 with:

  • Chrome 80.0 + NVDA 2019.3.1
  • Chrome 80.0 + JAWS 18.0
  • Chrome 80.0 + ChromeVox 53.0 (Chrome extension)
  • Firefox 74.0 + NVDA 2019.3.1
  • Firefox 74.0 + JAWS 18.0
  • IE 11.0 + NVDA 2019.3.1 (OK but sometimes reads the same text multiple times)
  • IE 11.0 + JAWS 18.0 (OK but sometimes reads the same text multiple times)
  • Windows Narrator - Kind of works but buggy. I didn't go into details

The most important things I've learned about ARIA live regions is that aria-live element must exist in the DOM on page load instead of being created by Javascript and also that there can be various issues if I decide to show/hide a live region instead of leaving it visible and only updating the content. That's why I was instead toggling the styles only by a class name. For example NVDA needs a small delay before a text update in a live region if the region was hidden and then shown. Otherwise the update is not recorded https://github.com/nvaccess/nvda/issues/8873. Then IE 11 somehow tends to repeat the live region content multiple times. Couldn't figure why. Also it was important to take care of the race conditions e.g. not to allow the text update into "Loading..." after the request was already done, or that a new request don't break the reporting of the current request.

I used the following code using Bootstrap 3.x and jQuery:

<!-- Hint: This element must not be added dynamically. Must be in the DOM initially so that a screen reader can register it -->
<div id="ajax-status-alert" aria-live="polite" data-toggle-class="alert alert-warning">
    <span class="ajax-status-alert__text"></span>
    <i class="ajax-status-alert__icon glyphicon glyphicon-refresh glyphicon-spin" role="presentation" aria-hidden="true"></i>
</div>
#ajax-status-alert {
    position: fixed;
    bottom: 10px;
    right: 15px;
    z-index: 1001;
}

.ajax-status-alert__text + .ajax-status-alert__icon {
    display: inline-block;
    margin-left: 5px;
}
$(document).ajaxStart(function (e) {
    console.log("LOG: FIRST AJAX REQUEST HAS STARTED"); // TODO: Remove this

    /* A delay to display a notification only if a request takes longer than X seconds */
    e.target.ajaxStatusAlertShowDelay = setTimeout(function () {
        let txtLoadingStarted = "Please wait. Loading..."; // TODO: Translate and clean-up already existing strings related to this functionality

        let $ajaxStatusAlert = $('#ajax-status-alert');
        let $ajaxStatusAlertText = $ajaxStatusAlert.find(".ajax-status-alert__text");
        let $ajaxStatusAlertIcon = $ajaxStatusAlert.find(".ajax-status-alert__icon");

        /* Reset and clean-up. Toggle class instead of show/hide otherwise expect issues with live region */
        $ajaxStatusAlert.attr("class", $ajaxStatusAlert.attr("data-toggle-class"));
        $ajaxStatusAlertText.removeClass("sr-only").text("");
        $ajaxStatusAlertIcon.show(0);
        console.log("LOG: RESET"); // TODO: Remove this

        $ajaxStatusAlertText.text(txtLoadingStarted);
        console.log("LOG: TEXT UPDATE"); // TODO: Remove this
    }, 1500); // TODO: Don't forget to update the value
});
$(document).ajaxStop(function (e) {
    console.log("LOG: ALL AJAX REQUESTS DONE"); // TODO: Remove this

    /* Prevent to show status alert */
    if ("undefined" !== typeof(e.target.ajaxStatusAlertShowDelay)) {
        clearTimeout(e.target.ajaxStatusAlertShowDelay);
    }

    let txtLoadingFinished = "Loading done."; // TODO: Translate

    let $ajaxStatusAlert = $('#ajax-status-alert');
    let $ajaxStatusAlertText = $ajaxStatusAlert.find(".ajax-status-alert__text");
    let $ajaxStatusAlertIcon = $ajaxStatusAlert.find(".ajax-status-alert__icon");

    /* Apply `sr-only` to loading done state so that it can remain visible to sr until read completely */
    $ajaxStatusAlertIcon.hide(0);
    if (!$ajaxStatusAlertText.hasClass("sr-only")) {
        $ajaxStatusAlertText.text(txtLoadingFinished).addClass("sr-only");
    }
    $ajaxStatusAlert.attr("class", "");
    console.log("LOG: TEXT UPDATE"); // TODO: Remove this
});

What about using a button instead of a link?

  • The issue remains the same. It does not make a difference if a user clicked a link or a button
  • From my perspective it depends much on the action whether we can assume it's more semantic to use a button or a link. If the action simulates a new page load, e.g. most of the page content will get updated, then a user expects such behavior, similarly to opening a new page and thus a link seems appropriate. It should be considered updating the page title and handling the history state for the browsers "back" button as well
  • If the action will only replace a small part of the page e.g. collapsing/expanding element then a button might be a better option. In such cases focus management might not even be a good idea despite the content update. It's better to let the user control the focus