Making Sense Of WAI-ARIA: A Comprehensive Guide
This article is a sponsored by Fable
The Web Accessibility Initiative — Accessible Rich Internet Applications (WAI-ARIA) is a technical specification that provides direction on how to improve the accessibility of web applications. Where the Web Content Accessibility Guidelines (WCAG) focus more on static web content, WAI-ARIA focuses on making interactions more accessible.
Interactions on the web are notorious for being inaccessible and are often part of the most critical functions such as:
- submitting a job application,
- purchasing from an online store, or
- booking a healthcare appointment.
I’m currently the Head of Accessibility Innovation at Fable, a company that connects organizations to people with disabilities for user research and accessibility testing and provides custom training for digital teams to gain the skills to build inclusive products.
As an instructor for accessible web development, I spend a lot of time examining the source code of websites and web apps and ARIA is one of the things I see developers misusing the most.
HTML
When you use HTML elements like input
, select
, and button
, there are two things you’ll get for accessibility: information about the element is passed to the DOM (Document Object Model) and into an Accessibility Tree. Assistive technologies can access the nodes of the accessibility tree to understand:
- what kind of element it is by checking its role, e.g., checkbox;
- what state the element is in, e.g., checked/not checked;
- the name of the element, e.g., “Sign up for our newsletter.”
The other thing you get when using HTML elements is keyboard interactivity. For example, a checkbox can be focused using the tab key and selected using the spacebar (specific interactions can vary by browser and operating system, but the point is they are available and standardized across all websites when you use HTML elements).
When you don’t use HTML, for example, if you build your own custom select using
s or you use a component library, you need to do extra work to provide information about the element and build keyboard interactivity for assistive technology users. This is where ARIA comes into play.
ARIA
Accessible Rich Internet Applications (ARIA) include a set of roles and attributes that define ways to make web content and web applications more accessible to people with disabilities.
You can use ARIA to pass information to the accessibility tree. ARIA roles and attributes don’t include any keyboard interactivity. Adding role="button”
to a
Roles
Let’s start with roles. What the heck is this thing in the code below?
<div className="dd-wrapper">
<div className="dd-header">
<div className="dd-header-title"></div>
</div>
<div className="dd-list">
<button className="dd-list-item"></button>
<button className="dd-list-item"></button>
<button className="dd-list-item"></button>
</div>
</div>
This is actually a snippet of code I found online from a select element for React. The fact that the element is completely unrecognizable from the code is exactly the issue that any assistive technology would have — it can’t tell the user what it is or how to interact with it because there’s no ARIA role.
Watch what we can do here:
<div className="dd-wrapper" role="listbox">
You might not be familiar with a listbox
, but it’s a type of select
that a screen reader user could recognize and know how to interact with. Now you could just use , and you wouldn’t have to give it a role because it’s already got one that the DOM and accessibility tree will recognize, but I know that’s not always a feasible option.
A role tells an assistive technology user what the thing is, so make sure you use the correct role. A button is very different from a banner. Choose a role that matches the function of the component you’re building.
Another thing you should know about ARIA roles is that they override an HTML element’s inherent role.
<img role="button">
This is no longer an image but a button. There are very few reasons to do this, and unless you exactly knew what you’re doing and why, I’d stay away from overriding existing HTML roles. There are many other ways to achieve this that make more sense from accessibility and a code robustness perspective:
<button><img src="image.png" alt="Print" /></button>
<input type="image" src="image.png" alt="Print" />
<button style="background: url(image.png)" />Print</button>
If you’re building a component, you can look up the pattern for that component in the ARIA Authoring Practices Guide which includes information on which role(s) to use. You can also look up all available roles in the mdn web docs.
In summary, if you’re building something that doesn’t have a semantic HTML tag that describes it (i.e., anything interactive built using
), it needs to have an ARIA role so that assistive technology can recognize what it is.
States And Properties (Aka ARIA Attributes)
In addition to knowing what an element is, if it has a state (e.g., hidden
, disabled
, invalid
, readonly
, selected
, and so on) or changes state (e.g., checked
/not checked
, open
/closed
, and so on), you need to tell assistive technology users what its current state is and its new state whenever it changes. You can also share certain properties of an element. The difference between states and properties isn’t really clear or important, so let’s just call them attributes.
Here are some of the most common ARIA attributes you might need to use:
aria-checked
It’s used with="true"
or="false"
to indicate if checkboxes and radio buttons are currently checked or not.aria-current
It’s used with="true"
or="false"
to indicate the current page within breadcrumbs or pagination.aria-describedby
It’s used with the id of an element to add more information to a form field in addition to its label.aria-describedby
can be used to give examples of the required format for a field, for example, a date, or to add an error message to a form field.
<label for="birthday">Birthday</label>
<input type="text" id="birthday" aria-describedby="date-format">
<span id="date-format">MM-DD-YYYY</span>
aria-expanded
It’s used with="true"
or="false"
to indicate if pressing a button will show more content. Examples include accordions and navigation items with submenus.
<button aria-expanded="false">Products</button>
This indicates that the Products menu will open a submenu (for example, of different product categories). If you were to code it like this:
<a href="/products/">Products</a>
You’re setting the expectation that it’s a link, and clicking it will go to a new page. If it’s not going to go to a new page, but it actually stays on the same page but opens a submenu, that’s what button plus aria-expanded
says to an assistive technology user. That simple difference between and
and the addition of
aria-expanded
communicates so much about how to interact with elements and what will happen when you do.
aria-hidden
It’s used with="true"
or="false"
to hide something that is visible, but you don’t want assistive technology users to know about it. Use it with extreme caution as there are very few cases where you don’t want equivalent information to be presented.
One interesting use case I’ve seen is a card with both an image and the text title of the card linking to the same page but structured as two separate links. Imagine many of these cards on a page. For a screen reader user, they’d hear every link read out twice. So the image links used aria-hidden="true"
. The ideal way to solve this is to combine the links into one that has both an image and the text title, but real-life coding isn’t always ideal, and you don’t always have that level of control.
Note that this breaks the fourth rule of ARIA (which we’ll get to in a bit), but it does it in a way that doesn’t break accessibility. Use it with extreme caution when there are no better workarounds, and you’ve tested it with assistive technology users.
aria-required
It’s used with="true"
or="false"
to indicate if a form element has to be filled out before the form can be submitted.
If you’re building a component, you can look up the attributes for that component on the ARIA Authoring Practices Guide. The mdn web docs covers states and properties as well as ARIA roles.
Keep in mind that all these ARIA attributes tell a user something, but you still have to code the thing you’re telling them. aria-checked="true"
doesn’t actually check a checkbox; it just tells the user the checkbox is checked, so that better be true or you’ll make things worse and not better for accessibility. The exception would be aria-hidden="true"
which removes an element from the accessibility tree, effectively hiding it from anyone using assistive technology who can’t see.
So now we know how to use ARIA to explain what something is, what state it’s in, and what properties it has. The last thing I’ll cover is focus management.
Focus Management
Anything interactive on a website or web app must be able to receive focus. Not everyone will use a mouse, trackpad, or touch screen to interact with sites. Many people use their keyboard or an assistive technology device that emulates a keyboard. This means that for everything you can click on, you should also be able to use the tab key or arrow keys to reach it and the Enter key, and sometimes the spacebar, to select it.
There are three concepts you’ll need to consider if you use
to create interactive elements:
- You need to add
tabindex="0"
so that a keyboard or emulator can focus on them. - For anything that accepts keyboard input, you need to add an event listener to listen for key presses.
- You need to add the appropriate role so that a screen reader user can identify what element you’ve built.
Remember that native HTML controls already accept keyboard focus and input and have inherent roles. This is just what you need to do when creating custom elements from non-semantic HTML.
Ben Myers does a deep dive into turning a div
into a button, and I’ll share parts of his example here. Notice the tabindex and the role:
<div tabindex="0" role="button" onclick="doSomething();">
Click me!
</div>
And you’ll need JavaScript to listen to the key presses:
const ENTER = 13;
const SPACE = 32;
// Select your button and store it in ‘myButton’
myButton.addEventListener('keydown', function(event) {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
event.preventDefault(); // Prevents unintentional form submissions, page scrollings, the like
doSomething(event);
}
});
When it comes to figuring out which keys to listen for, I suggest looking up the component you’re building in the ARIA Authoring Practices Guide and following the keyboard interaction recommendations.
Common Mistakes
Having looked at a lot of code in my lifetime, I see some accessibility errors being made repeatedly. Here’s a list of the most common mistakes I find and how to avoid them:
Using An aria-labelledby
Attribute That References An ID That Doesn’t Exist
For example, a modal that has a title in the modal but aria-labelledby
is referencing something else that no longer exists. It’s probably something removed by another developer who didn’t realize the aria-labelledby
connection was there. Instead, the modal title could’ve been an
and either aria-labelledby
could reference the
or you could set the focus on the
when the modal opens and a screen reader user would know what’s going on as long as role="dialog”
was also used. Try to avoid fragile structures that, if someone else came along and edited the code, would break easily.
Not Moving The Focus Into The Modal When It Opens
Countless times I’ve seen a screen reader user navigating the page behind the modal either unaware a modal has opened or confused because they can’t find the contents of the modal. There are several ways to trap focus within a modal, but one of the newer methods is to add inert
to the landmark (and, of course, make sure the modal isn’t inside
).
Inert
is getting better support across browsers lately. To learn more, check out Lars Magnus Klavenes’ Accessible modal dialogs using inert
.
Adding Roles That Duplicate HTML
In general, doing something like this is pointless. There is one case where it might make sense to do this. VoiceOver and Safari remove
list
element semantics when list-style: none
is used. This was done on purpose because if there is no indication to a sighted user that the content is a list, why tell a screen reader user that it’s a list? If you want to override this, you can add an explicit ARIA role="list"
to the
- .
Adrian Roselli says an unstyled list not being announced as a list “…may not be a big deal unless user testing says you really need a list.” I agree with him on that point, but I’m sharing the fix in case your user testing shows it’s beneficial.
Adding tabindex="0"
To Every Element
Sometimes developers start using a screen reader and assume that tabbing is the only way to navigate; therefore, anything without tabindex isn’t accessible. This is NOT true. Remember, if you don’t know how to use a screen reader, you can’t troubleshoot usability issues. Meet with an everyday screen reader user to figure those out.
Using Child Roles Without Parent Roles
For example, role="option"
must have a direct parent with role="listbox"
.
<div role="listbox">
<ul>
<li role="option">
The above code isn’t valid because there’s a
- between the parent and child elements. This can be fixed by adding a presentation role to essentially hide the
from the accessibility tree, like
.
Using role="menu"
For Navigation
Website navigation is really a table of contents and not a menu. ARIA menus are not meant to be used for navigation but application behavior like the menus in a desktop application. Instead, use