5 things I learned about WAI-ARIA (while implementing it myself)
I initially submitted this as a talk. Since it was not accepted I turned it into this blog post instead.
Like most web developers I had heard of ARIA. I had skimmed through the specs. But honestly, the boostrap documentation is still my go-to resource for actual usage. Some day I wanted to implement some assistive technology myself. This is when I found some unexpected quirks.
In my opinion, most of these quirks should be fixed. But maybe there are legitimate reasons why things have been defined this way. If you have an opinion on any of these, I would love to hear from you!
Building my own assistive technology
Most screen readers let you navigate along the headings, links, or landmarks in a page. Since visual scanning is impossible for most visually impaired users, this feature is hugely important in a screen reader.
My feeling was that this feature, especially landmark navigation, could also be useful for other users who might not want to install a full-fledged screen reader. There is this whole world of navigation semantics that is not exposed on regular browsers. At the very least, this could be a useful debugging tool for web developers like myself.
So I created a11y-outline as a web extension. In order to do this I had to go down the rabbit hole and look into specs that are normally only relevant for browser vendors.
The extension even got some positive feedback. If you try it yourself and find any bugs, please make sure to report them.
ARIA primer
So, what is this ARIA thing?
First, ARIA is an ontology that describes interactive user interfaces. Second, It also defines a mapping of common HTML semantics to this ontology. For example, an <h1>
element has the role "heading" and the level 1.
Third, it adds a way to explicitly overwrite the default semantics. For example, <h1>
is the same as <div role="heading" aria-level="1">
.
It is important to note that ARIA does only define semantics, not behavior. <div role="button">
will not automatically behave like a button. You have to implement the behavior in JavaScript, or better, use a <button>
element directly.
If you can use a native HTML element or attribute with the semantics and behavior you require already built in […] then do so. — https://www.w3.org/TR/using-aria/#rule1
In other words: ARIA is only for cases where there is no matching HTML element.
ARIA is designed in layers. On the one end we have HTML (or other host languages). On the other end we have the accessibility APIs of the different operating systems. The layers in between are defined in different specs:
- The ontology is defined in WAI-ARIA.
- The mapping between HTML and ARIA is defined in the HTML Accessibility API Mappings (HTML-AAM).
- The mapping between ARIA and OS APIs is defined in the Core Accessibility API Mappings (Core-AAM).
- Some additional parts have been split out, e.g. the Accessible Name and Description Computation.
Quirks
Now with the introduction out of the way, let's get to the weird stuff. We start slow though.
Landmarks
Landmark is a category of roles. Here is a typical layout for a page:
<header>
<nav>…</nav>
</header>
<main>
…</main>
<footer>
…</footer>
These elements have the roles "banner", "navigation", "main", and "contentinfo" respectively. Each one of them is a landmark. As described before, tools like screen readers (and a11y-outline) generate a navigation from these landmarks.
If you have more than one of any kind of landmark, you should add a label to each one of them:
<nav aria-label="Skip to Content">…</nav>
<nav aria-label="Main">…</nav>
You should not include the name of the landmark itself in the label as this will be added by the AT:
<nav aria-label="Main">Good</nav>
<nav aria-label="Main navigation">Bad</nav>
The weird part is that the mapping of HTML to ARIA is not always straight forward. For example, <header>
and <footer>
do not get the roles described above in some contexts:
<article>
<header>Not a banner</header>
…</article>
Forms, on the other hand, only become landmarks if they have an explicit label:
<form aria-label="Login">
<input name="username">
<input type="password" name="password">
<input type="submit" value="Login">
</form>
Accessible name calculation
Each element has a name and description. The algorithem to calculate them is crazy complicated as it contains recursion and tons of special cases. My own implementation is not fully compliant with the spec. But the spec is still evolving and even browsers are not fully compliant, so I guess that is fine.
Let's look at some examples. (Decide for yourself which of them are quirky!)
<a title="foo">bar</a>
The name here is "bar". The title
attribute is used as a fallback only in very rare cases.
<a aria-label="baz" title="foo">bar</a>
aria-label
trumps content here, so the name is "baz".
<a aria-label="" title="foo">bar</a>
Providing an empty aria-label
is the same as providing none at all. So the name goes back to "bar". (This means that you can not force an empty name.)
<a title="foo" hidden>bar</a>
The name for hidden elements is empty.
<div aria-labelledby="label">foo</div>
<div id="label" hidden>bar</div>
Did I just say hidden elements have an empty name? Well, of course there is an exception: If referenced directly, the name is calculated as if the element was not hidden. So the name for the first element is "bar".
<div id="test1" aria-labelledby="test2">foo</div>
<div id="test2" aria-labelledby="test3">bar</div>
<div id="test3" aria-labelledby="test1">baz</div>
To avoid infinite recursion, only one reference is followed. So the name for the first element is again "bar".
<label for="test">foo</label>
<label>
bar<input id="test" />
</label>
<label for="test">baz</label>
This is a fun one! Did you know that inputs could have more than one label? The name of the input is actually "foo bar baz".
<a aria-owns="lost-child">
<span>foo</span>
<span>bar</span>
<a>
<span id="lost-child">baz</span>
A child referenced via aria-owns
is just appended to the end. So the name is "foo bar baz" again.
<label>
What is the best metasyntactic variable?<select>
<option selected>foo</option>
<option>bar</option>
<option>baz</option>
</select>
</label>
If I follow the spec to the word the name for the select element should be "What is the best metasyntactic variable? foo bar baz". However, I am pretty sure this is not intended. Probably only the selected option should be included.
CSS is kinda relevant
Ok, this is web 1x1, right? HTML is for content, CSS is for presentation, JS is for behavior. So the ARIA semantics should surely not be influenced by CSS, right?
No quite.
Let me start with an example where the separation still holds. The order
property was specifically designed to allow authors to change the visual order while maintaining the source order:
The order property does not affect ordering in non-visual media (such as speech). Likewise, order does not affect the default traversal order of sequential navigation modes (such as cycling through links, see e.g. tabindex). — https://drafts.csswg.org/css-flexbox-1/#order-accessibility
<div style="display: flex">
<article style="order: 2">Article</article>
<nav style="order: 1">Nav</nav>
<aside style="order: 3">Aside</aside>
</div>
Now for the exceptions: If an element is hidden by CSS, it is also hidden in ARIA.
<div style="display: none">This is hidden</div>
Likewise, content that is added in CSS is also present in ARIA.
.test::before {
content: "foo"
}
<div class="test">bar</div>
Finally, there is currently discussion over whether the value of the display
property should influence whether whitespace should be added between elements when calculating names.
<div><p>foo</p><p>bar</p></div> <!-- foo bar -->
<p><em>un</em>subscribe</p> <!-- unsubscribe -->
HTML-AAM overrides other specs
Remember how I wrote about this nice hierarchy in the beginning? It was HTML - HTML-AAM - ARIA - Core-AAM - OS APIs. Well …, that is not actually true.
The HTML Accessibility API Mappings actually define mappings directly to the OS APIs. It is just that ARIA is used in many places.
<input value="foo" title="bar" hidden>
Remember how I wrote that name calculation, especially for form elements, was crazy complicated? HTML-AAM just adds some special cases that use rules which are completely different than the default algorithm. In this case, the otherwise rarely used title
attribute wins. The hidden
flag is simply ignored.
Conflicts
Ready for the final quirk? In my opionion this is the worst one and also the one that is probably the hardest to fix.
Guess whether this element is disabled or not:
<input disabled aria-disabled="false">
If you think that it is not disabled because aria-disabled
overwrites the default mapping, you are sadly not correct. Someone decided that while explicit ARIA attributes overwrite things most of the time, there are some poorly documented exceptions:
When a host language declares a WAI‑ARIA attribute to be in direct semantic conflict with a native attribute for a given element, user agents MUST ignore the WAI‑ARIA attribute and instead use the host language attribute with the same implicit semantic. — https://www.w3.org/TR/core-aam-1.1/#mapping_conflicts
Conclusion
ARIA contains some unexpected bits and even some bugs that result from high complexity. Maybe this complexity could be reduced in some places.
Still, ARIA is an incredible set of specifications. Not the least because they try to tackle an issue that inherently is very complex. It is also a set of specifications that is still evolving. I hope that some of the quirks discussed in the post will be fixed along the way.