When a web page is loaded, screen readers (like the one that comes with OS X, or JAWS on Windows) will read the content of the whole page. But say your page is dynamic, and as users performing an action, new content gets added to the page. For the sake of simplicity, say you display a message somewhere in a <span>
. How can you get the screen reader to read that new message?
-
Screen readers do read form fields and links when they get the focus. So one possibility is to put an anchor around the text and set the focus to that anchor. With CSS you can get the link not to show as a link for users looking at the page. But this method isn't very satisfactory as screen reader users will be falsely led to believe that this is a link. – avernet Sep 08 '10 at 17:58
-
3Add tabindex to any element and it will become readable, I believe (tabindex=-1 makes it scriptable but not tabbable). I often programmatically send focus to new content after a link is clicked (like a tab switcher or accordion) -- but it doesn't have to be a link to be focusable. Read up on tabindex. – Marcy Sutton Apr 27 '11 at 18:52
3 Answers
The WAI-ARIA specification defines several ways by which screen readers can "watch" a DOM element. The best supported method is the aria-live
attribute. It has modes off
, polite
,assertive
and rude
. The higher the level of assertiveness, the more likely it is to interrupt what is currently being spoken by the screen reader.
The following has been tested with NVDA under Firefox 3 and Firefox 4.0b9:
<!DOCTYPE html>
<html>
<head>
<script src="js/jquery-1.4.2.min.js"></script>
</head>
<body>
<button onclick="$('#statusbar').html(new Date().toString())">Update</button>
<div id="statusbar" aria-live="assertive"></div>
</body>
The same thing can be accomplished with WAI-ARIA roles role="status"
and role="alert"
. I have had reports of incompatibility, but have not been able to reproduce them.
<div id="statusbar" role="status">...</div>

- 5,742
- 2
- 21
- 26

- 9,165
- 5
- 47
- 56
-
7Just to be clear, the only valid values for `aria-live` are `off`, `polite` and `assertive`. If anyone is curious about the defaults for roles associated with live regions or even just wants to see what they do, I wrote a little playground that shows live regions in action: http://schne324.github.io/live-region-playground/ – harris Jan 12 '16 at 15:07
-
This don't work for me in Mac with Firefox 41.0.2 do you have any clue? Works fine with Safari and Chrome – Adrian Cid Almaguer Jul 07 '16 at 15:56
-
1Just to add on to the valid values for aria-live, there are actually 4 values. **off**, **polite**, **assertive**, and **rude** as noted from the docs here: https://msdn.microsoft.com/en-us/library/windows/apps/hh465711.aspx – Natasha Loiseau Feb 24 '17 at 16:11
-
4The WAI standard has 3 states (off, polite, assertive) https://www.w3.org/TR/wai-aria/states_and_properties#aria-live Microsoft adds the non-standard "rude", which appears to match the standard "assertive" – KevinButler May 10 '17 at 12:00
-
Just thought I'd provide an updated link for valid `aria-live` values. At the time of this comment, they are indeed **off**, **polite**, and **assertive**: [https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live#values](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live#values) – sam hooper Jan 03 '22 at 12:23
It really depends whether you are just adding some messages or replacing large parts of the page.
Messages
There are Aria Live Regions, which announce any change to their contents. This is very useful for status messages and sometimes even used with visually hidden live regions to only address screen reader users.
<button onclick="document.querySelector('#statusbar').innerHTML = new Date().toString()">Update</button>
<div id="statusbar" aria-live="assertive"></div>
The aria-live
attribute establishes a live region and its value is a politeness setting, which regulates how likely it is the change will interrupt what is currently being spoken by the screen reader.
Another classic example is inline validation of form fields, where the alert role, a live region, is used to immediately announce the error message to the user:
<label>Day of the week we hate
<input type="text" aria-describedby="error">
</label>
<div role="alert" id="error" hidden>only Monday is permitted</div>
Large Parts of Content
When JavaScript is changing large parts of the site, like in single page applications, putting everything inside a live region would be overkill and actually very annoying.
To let the user know that content changed after activating a trigger, two approaches exist:
- A new state of the trigger is announced, implying that the user can simply continue reading to find the new content
- Focus is put either onto the element who’s content changed, or on the first interactive element inside
Simply read on
The first case would be applied if the role of the trigger (or other status information) makes it clear that a content change will happen, so it’s expected.
A classic example is the accordion. It has aria-expanded
state, which communicates whether its contents are currently visible or not. If they are, the user will simply continue reading, because contents should follow immediately after.
toggleAccordion = e => {
const target = document.getElementById(e.currentTarget.getAttribute('aria-controls'));
e.currentTarget.setAttribute('aria-expanded', ! target.toggleAttribute('hidden'));
}
<!-- soon to be replaced by <details> and <summary> -->
<button aria-expanded="false" aria-controls="accordion-content" onclick="toggleAccordion(event)">2.1 First Rule of ARIA Use</button>
<blockquote id="accordion-content" hidden>
<p>If you can use a native HTML element [HTML51] or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so. […]</p>
</blockquote>
Put focus on the new content
In the second case focus is set programmatically elsewhere, so that element will be announced. This is particularly helpful if it’s parent elements have grouping roles, so their names will be announced as well, as in the case of a modal dialog.
Another example would be a single page application’s navigation, where the single navigation items are still navigated by means of tab.
To be able to programmatically focus a non-interactive element, but not manually, tabindex="-1"
is necessary. Focussing the headline is a best practice.
/* some sort of SPA router */
document.querySelectorAll('nav a').forEach(a => a.addEventListener('click', e => {
// hide all visible contents
document.querySelectorAll('main > :not([hidden])').forEach(c => c.hidden = true);
document.querySelectorAll('[aria-current]').forEach(c => c.removeAttribute('aria-current'));
// show selected content
const content = document.querySelector(e.currentTarget.getAttribute('href'));
content.hidden = false;
content.querySelector('h1').focus();
e.currentTarget.setAttribute('aria-current', 'page');
}));
a[aria-current] { font-weight: bold }
<nav>
<ul>
<li><a href="#page-1" aria-current="page">Page 1</a></li>
<li><a href="#page-2">Page 2</a></li>
</ul>
</nav>
<main>
<div id="page-1">
<h1 tabindex="-1">Page 1</h1>
<p>Many lines of content to follow</p>
</div>
<div id="page-2" hidden>
<h1 tabindex="-1">Page 2</h1>
<p>Many lines of content to follow</p>
</div>
</main>

- 4,783
- 2
- 26
- 51
Here is an adapted real world example -- this up-level markup has already been converted from an unordered list with links into a select menu via JS. The real code is a lot more complex and obviously could not be included in its entirety, so remember this will have to be rethought for production use. For the select menu to be made keyboard accessible, we registered the keypress & onchange events and fired the AJAX call when users tabbed off of the list (beware of browser differences in timing of the onchange event). This was a serious PITA to make accessible, but it IS possible.
// HTML
<!-- select element with content URL -->
<label for="select_element">State</label>
<select id="select_element">
<option value="#URL_TO_CONTENT_PAGE#" rel="alabama">Alabama</option>
</select>
<p id="loading_element">Content Loading</p>
<!-- AJAX content loads into this container -->
<div id="results_container"></div>
// JAVASCRIPT (abstracted from a Prototype class, DO NOT use as-is)
var selectMenu = $('select_element');
var loadingElement = $('loading_element');
var resultsContainer = $('results_container');
// listen for keypress event (omitted other listeners and support test logic)
this.selectMenu.addEventListener('keypress', this.__keyPressDetector, false);
/* event callbacks */
// Keypress listener
__keyPressDetector:function(e){
// if we are arrowing through the select, enable the loading element
if(e.keyCode === 40 || e.keyCode === 38){
if(e.target.id === 'select_element'){
this.loadingElement.setAttribute('tabIndex','0');
}
}
// if we tab off of the select, send focus to the loading element
// while it is fetching data
else if(e.keyCode === 9){
if(targ.id === 'select_element' && targ.options[targ.selectedIndex].value !== ''){
this.__changeStateDetector(e);
this.loadingElement.focus();
}
}
}
// content changer (also used for clicks)
__changeStateDetector:function(e){
// only execute if there is a state change
if(this.selectedState !== e.target.options[e.target.selectedIndex].rel){
// get state name and file path
var stateName = e.target.options[e.target.selectedIndex].rel;
var stateFile = e.target.options[e.target.selectedIndex].value;
// get the state file
this.getStateFile(stateFile);
this.selectedState = stateName;
}
}
getStateFile:function(stateFile){
new Ajax.Request(stateFile, {
method: 'get',
onSuccess:function(transport){
// insert markup into container
var markup = transport.responseText;
// NOTE: select which part of the fetched page you want to insert,
// this code was written to grab the whole page and sort later
this.resultsContainer.update(markup);
var timeout = setTimeout(function(){
// focus on new content
this.resultsContainer.focus();
}.bind(this), 150);
}.bind(this)
});
}

- 907
- 12
- 22