WAI-ARIA

An introduction to Accessible Rich Internet Applications

WAI-ARIA

An introduction to Accessible Rich Internet Applications

Prem Nawaz Khan

what is ARIA
and why do we need it?

the problem

it's "easy" (in most cases) to make static web content accessible, but nowadays the web strives to be an application platform

complex web applications require structures (e.g. interactive controls) that go beyond what regular HTML offers (though some of this introduced with HTML5 ... more on that later)

PayPal UI React Components

PayPal.com components

Zettle Otto Components

The problem

  1. Home pages with ARIA present averaged 70% more detected errors than those without ARIA
  2. Increased ARIA usage on pages correlated to higher detected errors. The more ARIA attributes that were present, the more detected accessibility errors could be expected.

The WebAIM Million - The 2022 report on the accessibility of the top 1,000,000 home pages

Accessibility is a broad subject

code examples here are simplified
(but will hopefully convey the right concepts)

Warning sign with lightning bolt and text: Danger 415 volts
<div onclick="...">Test</div>

faked button

for a sighted mouse / touchscreen user this is a button...
but what about keyboard users?

<div tabindex="0" onclick="...">Test</div>

faked button with focus

now we can at least focus it...but can we activate it?

<div tabindex="0" onkeyup="..." onclick="...">Test</div>

faked button with focus and keyboard handling

for a sighted mouse / touchscreen / keyboard user this is a button...
but what about assistive technology users?

compare <div> to a real <button>

faked button versus real <button>

"test"
versus
"test button – to activate press SPACE bar"

the problem

generic/inappropriate HTML elements, with extra JavaScript/CSS on top...but they're still recognised and exposed as <span>, <div>, etc

the interplay of browser and assistive technology

Diagram: when focused on an element, such as a button, the browser exposes information (name, value, role, state, description) to the OS accessibility API, which in turn is accessed by assistive technologies like JAWS

Operating systems provide interfaces that expose information about objects and events to assistive technologies
(e.g. Microsoft Active Accessibility [MSAA], the Mac OS X Accessibility Protocol [AXAPI], IAccessible2 [IA2])

Accessibility Tree

separate from the DOM, browsers build a matching "accessibility tree":

Marco Zehe: Why accessibility APIs matter

Assistive Technologies - (AT)

Assistive Technologies

inspection tools

inspection tools

test using assistive technologies (e.g. screenreaders), however...

assistive technologies often use heuristics to repair incomplete/broken accessibility API information - so we want to check what's actually exposed to the OS/platform.

of course, browsers also have bugs/incomplete support...

Firefox Accessibility Inspector panel in Firefox Developer Tools, showing the accessibility tree and accessibility properties - exposed by Firefox to the platform/OS accessibility API - of an element in a page

Firefox Accessibility Inspector (version 61+)

Chrome DevTools' accessibility panel, showing the accessibility tree and accessibility properties - exposed by Chrome to the platform/OS accessibility API - of an element in a page

Chrome DevTools Accessibility panel

Xcode's Accessibility Inspector, showing accessibility tree/attributes for a selected element in Chrome

Xcode Accessibility Inspector
(but for Chrome, remember to turn on accessibility mode in chrome://accessibility)

compare <div> to a real <button>

faked button versus real <button>

if you use custom (not standard HTML) widgets,
use ARIA to ensure correct info is exposed

what is ARIA?

W3C - Accessible Rich Internet Applications (WAI-ARIA) 1.1

ARIA defines HTML attributes to convey correct role, state, properties and value

Diagram: ARIA defines 'role' and 'aria-*' attributes

relationship between user agents (e.g., browsers), accessibility APIs, and assistive technologies

Diagram: the complete (and complex) ARIA RDF model

W3C - WAI-ARIA 1.1 - 5.3 Categorization of Roles

the whole model is vast and complex...and thankfully you don't need to remember this

Roles are categorized as follows:

Screenshot: the complete definition for the 'button' role in ARIA 1.1, listing - among other things - its 'Supported States and Properties' and 'Inherited States and Properties'

each role has "states and properties" (e.g. ARIA 1.1 definition for button)
implicit/inherited or defined via aria-* attributes

Basically What is this thing and What does it do?

what information does ARIA provide?

this information is mapped by the browser to the operating system's accessibility API and exposed to assistive technologies.

extra benefit: once AT understands meaning/purpose, it can automatically announce specific hints and prompts
(e.g. JAWS "... button - to activate, press SPACE bar")

<div tabindex="0"
role="button" onkeyup="..."
onclick="...">Test</div>

Demo: faked button with appropriate role

Chrome Developer Tools' accessibility panel, showing that a faked button using a 'div', but with correct role=button, is exposed the same way as a native HTML 'button' element

use ARIA to:

ARIA roles and attributes: restrictions

what ARIA doesn't do...

ARIA is not magic: it only changes how assistive technology interprets content. specifically, ARIA does not:

all of this is still your responsibility...

no ARIA is better than bad ARIA

W3C - WAI-ARIA Authoring Practices 1.2

WAI-ARIA spec can be dry/technical - use for reference
ARIA Authoring Practices Guide (APG) more digestible.

in principle ARIA can be used in all markup languages
(depending on browser support)

ARIA in HTML

W3C - ARIA in HTML

W3C - Using ARIA

the 5 rules of ARIA use

1. don't use ARIA

If you can use a native HTML element [HTML5] or attribute with the semantics and behaviour 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.

you can use a <span> to behave as, and be exposed just like, a link...


<span tabindex="0" role="link"
      onclick="document.location='...'"
      onkeyup="...">link</span>

example: link

...but why would you?

unless there's a very good reason, just use <a href="...">...</a>

You can't create a button by Nicholas Zakas

2. don't change native semantics

unless you really have to / know what you're doing

don't do this:

<h1 role="button">heading button</h1>

otherwise the heading is no longer a heading
(e.g. AT users can't navigate to it quickly)

you can do this instead:

<h1><span role="button">heading button</span></h1>

or, in accordance with the first rule or ARIA:

<h1><button>heading button</button></h1>

example: heading button

don't do this:

<ul role="navigation">
    <li><a href="...">...</a></li>
    ...
</ul>

do this instead:

<div role="navigation">
    <ul>
        <li><a href="...">...</a></li>
        ...
    </ul>
</div>

or list is no longer a list (e.g. AT won't say "list with X items...")

example: list navigation

3. make interactive ARIA controls keyboard accessible

All interactive widgets must be focusable and scripted to respond to standard key strokes or key stroke combinations where applicable. [...]

Refer to design patterns and widgets sections of the ARIA Authoring Practices Guide

4. don't "neutralise" focusable elements

don't use role="presentation" or aria-hidden="true" on a visible focusable element. Otherwise, users will navigate/focus onto "nothing".


<!-- don't do this... -->

<button role="presentation">press me</button>

<button aria-hidden="true">press me</button>

<span tabindex="0" aria-hidden="true">...</span>

example: neutralised elements

Screenshot of Chrome Developer Tools' accessibility panel, showing how aria-hidden elements, and elements whose ancestor is aria-hidden, are denoted

Chrome DevTools indicates when a node is hidden
(directly, or due to an ancestor being hidden)

Screenshot of Firefox accessibility inspector - aria-hidden nodes are not shown at all in the accessibility tree representation

aria-hidden removes nodes from the accessibility tree
(as seen here in Firefox's accessibility inspector)

5. interactive elements must have an accessible name


<!-- don't do this... -->

<span tabindex="0" role="button">
    <span class="glyphicon glyphicon-remove"></span>
</span>

<span tabindex="0" role="button">
    <span class="glyphicon glyphicon-remove">
        <span class="sr-only">Delete</span>
    </span>
</span>

<span tabindex="0" role="button" title="Delete">
    <span class="glyphicon glyphicon-remove"></span>
</span>

<span tabindex="0" role="button" aria-label="Delete">
    <span class="glyphicon glyphicon-remove"></span>
</span>

<span tabindex="0" role="button" aria-labelledby="lby">
    <span class="glyphicon glyphicon-remove"></span>
</span>
...
<span id="lby" class="...">Delete</span>
Screenshot of Chrome Developer Tools' accessibility panel, showing details about which attributes - content, title, aria-label, aria-labelledby - contribute to the element's name

Chrome DevTools' accessibility panel can help understand which attributes contribute to an element's name

example (Demo) : accessible name calculation

Quiz: What's my name?



<label for="name" title="Ginny" aria-label="Voldemort" 
 aria-labelledby="hname">Harry Potter</label>

<input type="text" id="name">
<span id="hname" hidden>Albus Dumbledore</span>

Demo:: Small Quiz

Quiz answer when an input contains all of labelling attributes

refer to WAI-ARIA 1.1 - 5.2.7. Accessible Name Calculation

aria-labelledby usage

please do use the native features wherever practical



<label for="fname">Name</label><input type="text" id="fname">
<img alt="Choclate">
<button>Continue</button>

aria-labelledby is one of secondary methods to label native HTML UI elements

Hiding Content Has No Effect on Accessible Name or Description Calculation


 <a href="home.html" aria-labelledby="unseen">
now you don't </a>

 <p id="unseen" hidden>Now you hear me </p>
Accessible name: "Now you hear me"

Self Reference


 <a href="home.html" aria-labelledby="unseen home" id="home">
now you do</a>

 <p id="unseen" hidden>Now you hear me </p>
Accessible name: "Now you hear me, now you do"

Multiple References


<a href="alphabet.html" id="pre" 
aria-labelledby="pre a b c">Easy as</a>

accessible name: "Easy as A B C"

<a href="alphabet.html" id="pre" 
aria-labelledby="pre c b a">Easy as</a>

accessible name: "Easy as C B A"

<span id="a">A</span> 
<span id="b">B</span>
<span id="c">C</span>
	

Prefer visible labels over hidden labels

use aria-labelledby when a visible label exist

<span id="detail1">consent granted 31 May 2021<span>
<a href="details.html" id="self" aria-labelledBy="self detail1">
	View details
</a>

Above is better than


<span>consent granted 31 May 2021<span>
<a href="details.html" 
	aria-label="View details consent granted by 31 May 2021">Details
</a>

ARIA and HTML5

ARIA and HTML5

Using Chrome DevTools' accessibility panel we see that a native HTML5 input with type='range' is exposed correctly as a 'slider'

example: HTML5 range input

W3C HTML Accessibility API Mappings 1.0

HTML5 accessibility

W3C Validator result, showing an error for an inappropriately used ARIA role

side note: you can validate pages with (static) ARIA validator.w3.org

common structures and widgets

(not an exhaustive list - enough to understand concepts)

using ARIA to provide structure


<p class="1" role="heading" aria-level="1">Heading 1</p>
...
<p class="h2" role="heading" aria-level="2">Heading 2</p>
...
<p class="h3" role="heading" aria-level="3">Heading 3</p>
...

example: headings


<div role="list">
    <div role="listitem">...</div>
    <div role="listitem">...</div>
    <div role="listitem">...</div>
    ...
</div>

example: list/listitem

<img> is identified as an image by assistive technologies, and you can provide alternative text.


<img src="..." alt="alternative text">

removing semantics

if your page/app uses inappropriate markup, ARIA can be used to remove semantic meaning. useful for remediation if markup cannot be changed.


<table role="presentation">
    <tr>
        <td>Layout column 1</td>
        <td>Layout column 2</td>
    </tr>
</table>

example: layout table remediation

ARIA 1.1 introduced role="none" as an alias for role="presentation" – they are equivalent (and older browsers/AT likely better off still using role="presentation")

landmarks

Classic blog structure using div elements with id attributes 'header', 'sidebar', 'content', 'footer' and individual posts marked up with div elements given class name 'post'

example: blog structure

Classic blog structure using div elements, now complemented with ARIA role attributes 'banner', 'navigation', 'main', 'contentinfo' and individual posts with role of 'article'
why define landmarks?
Illustration of how the blog structure with ARIA role attributes is listed in NVDA's Elements List > Landmarks

example: blog structure with ARIA

doesn't HTML5 solve this?

Classic blog structure, recoded using HTML5 structural elements: header, nav, main, article, footer

example: blog structure with HTML5

Illustration of how the blog structure with HTML5 structural elements is listed in NVDA's Elements List > Landmarks

using ARIA for
simple/standalone widgets

button


<span class="...">Button?</span>

<div class="...">Button?</div>

<a href="#" class="...">Button?</a>

example: button

while using a link is slightly less evil, as at least it receives keyboard focus by default, it's still not correct: links are meant for navigation, not in-page actions or form submissions


<span tabindex="0" class="..." role="button">Button!</span>

<div tabindex="0" class="..." role="button">Button!</div>

<a href="#" class="..." role="button">Button!</a>

assuming there's a click handler:


foo.addEventListener('keyup', function(e) {
  // Space key
  if (e.key === " ") {
    // stop default behavior (usually, scrolling)
    e.preventDefault();
    // trigger the existing click behavior
    this.click();
  }
});

you could even do it "in one go" for all your faked buttons, assuming they have the correct role="button", with querySelectorAll and attribute selectors:


var buttons = document.querySelectorAll("[role='button']");
for (var i=0; i<buttons.length; i++) {
  buttons[i].addEventListener('keyup', function(e) {
    if (e.key === " ") {
      e.preventDefault();
      this.click();
    }
  });
}

toggle button

let's assume we implement this with JavaScript to purely add a CSS classname:


<button class="...">Toggle</button>

<button class="... toggled">Toggle</button>

example: toggle

in real applications, you'll likely keep track of the state in some additional way – this is only for illustrative purposes


<button class="..." aria-pressed="false">Toggle</button>
<button class="... toggled" aria-pressed="true">Toggle</button>
foo.getAttribute("aria-pressed");
foo.setAttribute("aria-pressed", "true");
foo.setAttribute("aria-pressed", "false");

add aria-pressed and dynamically change its value

example: toggle with aria-pressed


<button class="... toggled" aria-pressed="true">Toggle</button>
button.toggled { ... }
button[aria-pressed="true"] { ... }

example: toggle with aria-pressed and simplified CSS

if your actual label text changes when toggling, aria-pressed is likely not necessary (could actually be more confusing for user)


<button ...>Mute</button>
if (...) {
    this.innerHTML = "Mute";
    ...
} else {
    this.innerHTML = "Unmute";
    ...
}

example: toggle with a changing name/label and ARIA versus toggle with a changing name/label only

checkbox


<span tabindex="0" class="...">Option</span>

<span tabindex="0" class="... checked">Option</span>

example: checkbox


<span tabindex="0" role="checkbox"
      aria-checked="false" class="...">Option</span>

<span tabindex="0" role="checkbox"
      aria-checked="true" class="... checked">Option</span>

example: checkbox with aria-checked

radio button


<span tabindex="0" class="...">Yes</span>
<span tabindex="0" class="... selected">No</span>
<span tabindex="0" class="...">Maybe</span>

example: radio button


<span tabindex="0" role="radio"
      aria-checked="false" class="...">Yes</span>
<span tabindex="0" role="radio"
      aria-checked="true" class="... selected">No</span>
<span tabindex="0" role="radio"
      aria-checked="false" class="...">Maybe</span>

example: radio button with ARIA (but note incomplete focus handling)

disclosure widget

<button ...>More details</button>
<div class="show" ...> ... </div>

example: disclosure widget

<button ... aria-expanded="true"
    aria-controls="disclosure1">More details</button>
<div class="show" ... id="disclosure1"> ... </div>

example: disclosure widget with aria-expanded / aria-controls

accordion

<button ... aria-expanded="true"
    aria-controls="accordion1">Item 1</button>
<div class="show" ... id="accordion1"> ... </div>

<button ... aria-expanded="false"
    aria-controls="accordion2">Item 2</button>
<div class="show" ... id="accordion2"> ... </div>

<button ... aria-expanded="false"
    aria-controls="accordion3">Item 3</button>
<div class="show" ... id="accordion3"> ... </div>

example: accordion using disclosure widgets

modal dialog


<button>Launch...</button>
...
<div ... >
    <div>My custom dialog</div>
    ...
</div>

example: modal dialog

...but focus handling is not really correct...

example: modal dialog with focus management

...but for assistive tech users, it's still not clear what is happening...


<button>Launch...</button>
...
<div role="dialog"
     aria-labelledby="modalDialogHeader"
     aria-describedby="modalDialogDescription" ... >
  <div id="modalDialogHeader">My custom dialog</div>
  <div id="modalDialogDescription">
    ... 
  </div>
  ...
</div>

example: modal dialog with focus management and ARIA

...almost perfect, but assistive tech users can still navigate out of the modal...

<div id="wrapper">
  <button>Launch...</button>
</div>
...
<div role="dialog" ...> ... </div>
function openModal() {
 document.getElementById("wrapper")
         .setAttribute("aria-hidden","true"); ...
}
function closeModal() {
 document.getElementById("wrapper")
        .removeAttribute("aria-hidden"); ...
}

example: modal dialog with aria-hidden

note: aria-hidden does not prevent regular keyboard focus!

function openModal() {
 document.getElementById("wrapper").setAttribute("inert","");
 ...
}
function closeModal() {
 document.getElementById("wrapper").removeAttribute("inert");
 ...
}

example: modal dialog with inert

note: inert does hide elements from accessibility tree and remove behavior such as keyboard focusability. however, not natively supported yet – use the inert polyfill


<button>Launch...</button>
...
<div role="dialog" aria-modal="true"
     aria-labelledby="modalDialogHeader"
     aria-describedby="modalDialogDescription" ... >
  <div id="modalDialogHeader">My custom dialog</div>
  <div id="modalDialogDescription">
    ... 
  </div>
  ...
</div>

example: modal dialog with aria-modal (new in ARIA 1.1)

...but you still need to do the focus management yourself...

see also: Scott O'Hara: The current state of modal dialog accessibility

managing focus

complex widgets and focus

there are two approaches for focus:

ARIA Authoring Practices Guide - Developing a Keyboard Interface


	<span tabindex="-1" role="radio"
		  aria-checked="false" class="...">Yes</span>
	<span tabindex="0" role="radio"
		  aria-checked="true" class="... selected">No</span>
	<span tabindex="-1" role="radio"
		  aria-checked="false" class="...">Maybe</span>
	

only one radio button inside the group has focus. changing the selection using CURSOR keys, dynamically changes tabindex, aria-checked and sets focus() on the newly selected radio button

example: ARIA Practices 1.2 - Radio Group Using Roving tabindex

not all complex widgets lend themselves to "roving" tabindex – e.g. role="combobox" needs aria-activedescendant, as actual focus must remain inside the textbox.

example: Combobox With List Autocomplete Example

this approach can be complex, and not always supported by assistive technologies (particularly on mobile).

live regions

making assistive technology
aware of content changes

<div id="output"></div>
var o = document.getElementById("output");
o.innerHTML = "Surprise!"; // show the notification

example: notification as result of button press

but how can AT users be made aware of the notification / content change?

one way to notify users of assistive technologies of new content (a new element added to the page, made visible, a change in text) is to move focus() programmatically to it


<div id="output" tabindex="-1"></div>
var o = document.getElementById("output");
o.innerHTML = "Surprise!"; // show the notification
o.focus(); // move focus to the notification

but this is not always possible, as it would interrupt the user's current actions...

example: notification via focus() and a more problematic example simulating a long-running function.

ARIA live regions

<div id="output" aria-live="polite"></div>
var o = document.getElementById("output");
o.innerHTML = "Surprise!"; // show the notification

example: notification via aria-live

bonus points: set aria-disabled="true" on the control, and optionally aria-busy="true" on the notification / section of the page that is getting updated. see notification via aria-live, with aria-busy and aria-disabled


<span role="status">
    some form of status bar message...
</span>

example: status bar


<span role="alert">
    an alert message (no user interaction)
</span>

example: alert

Testing role="alert"

That works consistently in all browser, AT combos

Modifying the innerHTML of a role="alert" element already in the DOM


errorMsg.innerHTML = "You need to fill in your password.";

Adding a new element with role="alert" to the DOM dynamically


el.insertAdjacentHTML('afterend', 
		'<div role="alert">You need to fill in your username</div>');

example: Dynamic alerts

Testing aria-live="assertive"

That works consistently in all browser, AT combos

Modifying the innerHTML of a aria-live="assertive" element already in the DOM


errorMsg.innerHTML = "You need to fill in your password.";

Making an invisible ([hidden] attribute) element already in the DOM visible


errorMsg.removeAttribute("hidden");

example: Dynamic aria-live

ARIA has many more
complex/composite widgets and structures

example:
tab panels


<div role="tablist" ...>
  <div role="tab" aria-controls="panel1"
       aria-selected="true"...>Tab 1</div>
  <div role="tab" aria-controls="panel2" ...>Tab 2</div>
  <div role="tab" aria-controls="panel3" ...>Tab 2</div>
</div>

<div role="tabpanel" id="panel1">...</div>
<div role="tabpanel" id="panel2" aria-hidden="true">...</div>
<div role="tabpanel" id="panel3" aria-hidden="true">...</div>

example: ARIA Practices 1.1 Tabs with Automatic Activation

variations: Marco Zehe: Advanced ARIA tip #1: Tabs in web apps

not appropriate if you're just marking up a site navigation...

as useful as ARIA is, it is far from perfect...

We have many ARIA attributes that are useful in theory but utter poop because they just don’t work across AT.

a11ysupport.io for AT incompatibilities

SVG Accessibility

3 Types

  1. Decorative
  2. Meaningful
  3. Interactive

Decorative

Use aria-hidden="true"


<button>
<svg xmlns="http://www.w3.org/2000/svg"  aria-hidden="true">
    <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165"></path>
  </svg>
<span class="sr-only">Hamburger menu</span>
</button>

Meaningful

Provide title...


<svg aria-labelledBy="docTitle">
 <title id="docTitle">Here is a meaningfull title</title>
 ... 
 </svg>

Provide description...


<svg aria-labelledBy="docTitle" aria-describedBy="docDesc">
 <title id="docTitle">Here is a meaningfull title</title>
 <desc id="docDesc"> Additional Description </desc>
 ... 
 </svg>

Interactive

Make it focusable with xlink and tabindex="0"...


<a xlink:href="http://paypal.com" tabindex="0">
<rect width="75" height="50" rx="20" ry="20" fill="#90ee90" 
stroke="#228b22" stroke-fill="1" />
<text x="35" y="30" font-size="1em" text-anchor="middle"  
fill="#000000">PayPal
</text>
</a>

If it is not a link, add a role to ensure it is semantic...


<text y="20" role="heading" aria-level="1">Heading level one</text>
For complex interactions, use combination of roles, tabindex="0", focusable="true"

<g aria-label="Startdate" role="slider"  
tabIndex="0" focusable="true" aria-orientation="horizontal" 
aria-valuemin="Jan 01, 2013" aria-valuemax="Apr 09, 2014" 
aria-valuetext="Mar 03, 2013" aria-valuenow="248.46153846153845">	

Demo: PayPal AmCharts Plugin

JS Frameworks

AngularJS Developer Guide - Accessibility with ngAria

React - Accessibility

Recap...

What ARIA is/isn't ...

pragmatic approach to using ARIA

get in touch

Twitter: @mpnkhan

Creative Commons: Attribution Non-Commercial Share-Alike