WAI-ARIA: the dark art of accessibility?

A few weeks back I was interviewed on the A11y Rules podcast by Nic Steenhout. In it, I discuss what I believe to be the biggest barrier to a more accessible World Wide Web, which I believe to be awareness. In my experience, it’s not that there is a lack of willingness by web developers to incorporate accessibility, but rather that there is much confusion and misunderstanding of how accessibility technically works and what is expected from web developers. And at an organisational level, there is often a commitment to accessibility, but little culture of accessibility throughout various teams that each have a role to play in creating accessible digital products.

I end the interview with Nic by saying that accessibility is not a dark art. That said, one area of accessibility that often stumps even the most experienced developers, is WAI-ARIA – the Accessible Rich Internet Applications (ARIA) specification.

So, combining these two thoughts together, this is an awareness-raising blog post for web developers about ARIA. It was prompted by an issue discovered on a client’s website that was using a carousel powered by Slick. I’m going to take a look at some of the ARIA used by Slick.

In summary (tl;dr)

One of Slick’s demos includes the following code hierarchy:


<div role="toolbar">
  <ul role="tablist">
    <li role="presentation" aria-selected="true" aria-controls="[invalid id]">
      <button role="button" tabindex="0">

In summary, the issues are:

  1. ARIA toolbar and tablist roles are mixed together.
  2. The tablist contains no elements with tab roles.
  3. The aria-selected state and aria-controls attributes are applied to inappropriate elements.
  4. aria-controls attributes reference element IDs that do not exist in the page.
  5. The <button> element’s role is duplicated by role="button".
  6. The tabindex="0" applied to the <button> is unnecessary.

What is WAI-ARIA?

The Accessible Rich Internet Applications specification is maintained by the WAI at the W3C. It provides a toolkit of attributes that allow us to enhance the native roles of HTML elements to build more accessible user experiences.

These days, HTML alone does not provide enough of a toolkit to build a desktop-like experience on the Web, so we build custom controls using lots of JavaScript. This is great, but to do so in an accessible manner, we need some additional tools to fill in the gaps in HTML’s teeth.

What’s your beef with Slick, huh?

Before I get started, I don’t mean to pick on Slick here, but it struck me as a good example of where even expert JavaScript developers can build for accessibility and still get things wrong.

The accessibility community doesn’t much like carousels as they embody several accessibility and usability issues, even when the individual components that make up a carousel are made technically accessible. This has resulted in resources like shouldiuseacarousel.com being born, demonstrating some of the issues.

Slick’s demos have obviously been built with accessibility in mind, which is great. However, some basic misunderstandings have resulted in some poor ARIA implementations.

Examining the code

In the interests of time and clarity, I’m not going to take you through every issue with Slick’s demos, but rather look at one part of the interface: the navigation dots for the carousel in the “Single Item” demo.

The navigation dots for Slick’s demo carousels are implemented as a tab list. We can extract the following DOM hierarchy from the HTML used:


<div role="toolbar">
  <ul role="tablist">
    <li role="presentation" aria-selected="true" aria-controls="[invalid id]">
      <button role="button" tabindex="0">

What are the problems here? Let’s examine this code snippet and try out Slick’s “Single Item” demo using NVDA screen reader.

Avoid mixing incompatible ARIA roles

The first two elements in the hierarchy represent a toolbar with a tab list nested inside it. These ARIA attributes are described in the specification as follows:

  • The ARIA toolbar role represents a collection of commonly used function buttons or controls represented in compact visual form.
  • The ARIA tablist role represents a set of tab elements.

It really doesn’t make sense to have a tablist role nested inside of a toolbar role. Using the toolbar role to contain the previous and next buttons that control a carousel is fine, but it should not mix with the semantics of a set of tabs.

ARIA interfaces should be self-contained and should not mix incompatible roles together.

Note: Technically speaking, toolbar implies a grouping structure role and tablist is composite widget role. A grouping role is permitted to wrap a widget, but that does not mean that it makes sense to do so – valid does not equal semantically appropriate.

Using ARIA roles is a promise

With great power comes great responsibility. Don’t misjudge ARIA – it is powerful. But using the semantics that ARIA provides is not the end of the story. Adding ARIA roles to your HTML is a promise to code in any expected roles, states and properties for that role, as well as to implement the expected behaviours using JavaScript.

The navigation dots are implemented as a set of tabs, which would make sense for a carousel where only a single item is displayed at any one time. Any element with a tablist role should contain elements with role="tab" that reference elements that have role="tabpanel". There are no tab controls. There are no tab panels. It’s a tab list containing buttons, which is functional but not expected.

Slick’s toolbar does not implement the best practice focus management for toolbars to provide the expected keyboard behaviours (using a roving tabindex implementation).

Discrete DOM nodes are responsible for communicating accessibility information

Accessibility information coded in your HTML is conveyed by browsers to assistive technology via accessibility APIs. These APIs convey graphical user interfaces as a hierarchical tree of accessibility information similar to – but not the same as – the DOM.

For each part of an ARIA-based widget, discrete nodes in the Document Object Model (DOM) are responsible for communicating accessibility information to assistive technology. For most controls and widgets, ARIA state and property attributes relevant to a particular role need to be applied to a single HTML element that is responsible for carrying accessibility information. When multiple HTML elements need to work together to form a widget, most of the time this needs to be achieved using programmatic relationships, such as aria-describedby, aria-controls, etc.

In our accessibility testing, we often see ARIA attributes related to a particular component strewn across multiple HTML elements. This is akin to putting the required attributes of an HTML <input> element somewhere other than on that <input> element. That just doesn’t make sense, right?

Taking a look at our code snippet from Slick, we can see an ARIA state (aria-selected) and an ARIA property (aria-controls) are both separated from the element to which the attributes are related. The list item to which these ARIA attributes are applied is marked as presentational, which essentially makes it semantically the same as a <div>. This is not an appropriate location to apply these attributes.

Note: Despite the role="presentation", NVDA announces the buttons as “bullet 1 button”, “bullet 2 button”, etc.

There is a set of aria-selected attributes on the navigation dots that toggle correctly as each slide of the carousel becomes visible. However, these attributes are incorrectly applied to the containing <li> element rather than the control itself. As a result, NVDA ignores the aria-selected attributes and does not announce which of the slides is currently active. The aria-selected attribute must always be added to an element that has a role. A selected <div> makes no sense. On the other hand, a selected tab does make sense.

Note: The aria-current attribute should not be used as a substitute for aria-selected on a set of tabs.

An aria-controls attribute is primarily intended for creating a programmatic relationship between a control and the element on the page that is controlled by it. For example, a tab controls a tab panel. As with the aria-selected attributes, the aria-controls attributes in the Slick code are incorrectly applied to the containing <li> element rather than the control itself. These attributes also reference element IDs that do not exist in the page.

Avoid duplicating HTML semantics with unnecessary ARIA attributes

The <button> element in the snippet of code we’re looking at includes two unnecessary ARIA declarations: role="button" and tabindex="0".

This breaks the first rule of ARIA use – use native HTML elements whenever possible – and provides no additional benefit by being applied to the button.

Why?

HTML’s <button> element has an implicit accessibility role of button, so there is no need to duplicate this using the role attribute.

HTML <button> elements are natively keyboard focusable. Applying tabindex="0" to an element causes that element to become keyboard focusable and part of the natural focus order of the page. The only reason we may see this is if the ARIA role being used expects a single tab stop, which is implemented by using a roving tabindex. This is the expected behaviour for a set of tabs, for example, so the tabindex attribute would actually belong in this code if it was implemented correctly as a set of tabs. However, examining the behaviour in the demo, the buttons used for the navigation dots all have tabindex="0" applied and never change to tabindex="-1".

Other issues

The seasoned accessibility experts reading this may notice other issues that exist with Slick’s demos, which include:

  • An aria-live region is used to wrap the content of the carousel slides, which causes NVDA to announce the content of every carousel in some circumstances (compare NVDA running with Chrome versus Firefox).
  • Listbox widgets and role="option" attributes are used to mark up each slide, which is a misunderstood implementation of the specification, which is intended to extend HTML’s native <select> elements.
  • The aria-label attributes applied to the previous and next buttons are unnecessary as they duplicate the text content inside the <button> element that already acts to provide an accessible name.
  • Poor visibility of keyboard focus.

Aside from the issues with ARIA semantics discussed above, Slick is described as being “fully accessible with arrow key navigation”. The expected method of operating many ARIA interfaces from the keyboard is to use your keyboard’s arrow keys. However, Slick’s demo carousels do not respond to presses of the arrow keys on a keyboard.

Something I mentioned in the podcast interview with Nic is that I don’t believe there is such as thing as “fully accessible”, so I try to avoid using that phrase. It’s no easy task making something accessible to every person and accommodate the way they interact with the world. You can only do your best to make things as accessible as you can.

So, um… not a dark art, huh?

I’ve covered quite a lot in this post by only looking at a small snippet of code. This may seem daunting to many web developers reading this, but ARIA really isn’t as hard as it may seem. I hope you feel like you now understand a bit more about ARIA and how to use it appropriately.

As with any aspect of coding, it’s important to understand your tools before you use them. In the case of ARIA, my advice is:

  1. Read me first because no ARIA is better than bad ARIA.
  2. Familiarise yourself with the rules for ARIA use in Notes on ARIA Use in HTML.
  3. Practice your art forms, even the dark ones.

Thanks

Thanks to Steve Faulkner for helping clarify a point about ARIA semantics… and also for turning my own confusion around ARIA into understanding many years ago.