Introduction
Howdy, axe-con! Thanks for joining in for my talk, Hijacking Screenreaders with CSS.
This talk is divided into three parts:
- Bend brains. I introduce mental models I built for myself when I was learning web development and accessibility, especially around the separation of content and presentation, and then I go over some weird demos that challenged those mental models.
- Dive deeper. To understand where this behavior comes from, we dive into the history of screenreaders, and we go on an odyssey into browser and operating system internals.
- Life lessons. Knowing what we know now about how content is exposed to assistive technologies, and about the history that led to seemingly unexpected behavior being built into the browsers, how does this change how we build for the web?
Demos
In my axe-con talk, I demonstrate a few cases, getting weirder as we go, where CSS has a clear, noticeable impact on screenreader output. I've provided those examples here. Each one has relevant code snippets, a video demonstrating the behavior, and a link to where you can play with the demos yourself.
Hiding Content
In what is probably the most well-known instance of CSS impacting screenreader announcements, hiding elements with display: none
, visually: hidden
, and some other techniques will result in those elements also being excluded from screenreader output. This behavior is consistent across all major browsers and screenreaders.
<p>
I’ll be read aloud!
</p>
<p style="display: none">
I’ll be skipped.
</p>
<p>
I will also be read aloud.
</p>
Adding Content with Pseudo-Elements
The very point of pseudo-elements is that they're not in the DOM. If assistive technologies like screenreaders were purely reading from the DOM, there's no way pseudo-elements could inject content into assistive technology output. However, pseudo-elements are factored into an element's accessible name computation. This behavior is consistent across all major browsers and screenreaders.
Test the button with a pseudo-element.
<button>Edit</button>
button::before {
content: '✏️';
}
Changing Pronunciation with Text Transforms
This one is contextual, and depends heavily on your browser and screenreader combination.
In this case, we have a button that reads "Add." After using CSS to make the whole word uppercase, VoiceOver pronounces the button by spelling out each letter: A-D-D. Ordinarily, screenreaders are pretty smart about pronouncing uppercased words as words, but in this case, VoiceOver isn't sure if "ADD" is a word, or an acronym for something like attention deficit disorder. In this particular case, its heuristics fail.
Test the button before applying capitalization, and then test the button after applying capitalization.
<button>Add</button>
<button>Add</button>
button {
text-transform: uppercase;
}
Removing List Semantics
This impacts specifically WebKit browsers.
In this case, a list with default styles will announce as a list, and tell the user how many items are in the list. Users also have the option to skip over the list as a whole. When you remove the list markers with CSS, however, the list forgets to be a list, and instead, each list item is announced as though it were just a generic <div>
.
Note: Some of this behavior has recently changed, and now lists inside of navigation elements maintain their semantics.
Test the list before removing bullets.
<ul>
<li> ... </li>
</ul>
Test the list after removing bullets.
<ul>
<li> ... </li>
</ul>
ul {
list-style: none;
}
Fixing Table Semantics
For this test to work, you need a <table>
element that doesn't make full use of table semantics — so no <th>
, <thead>
, <tbody>
, <caption>
, or anything of the sort. Using the default table styles for these low-semantics tables results in the table being more or less announced as a series of <div>
s, with none of the other behavior screenreader users would expect from tables (such as being provided a row count; being able to navigate up, down, left, and right; and being able to skip the table entirely).
However, if you do anything to style the table to look more tablelike, such as zebra striping, the table semantics come back, and the user is now able to navigate the table as though it's a table.
Test the table with default styles.
<table>
<tr> ... </tr>
</table>
Test the table with zebra striping.
<table>
<tr> ... </tr>
</table>
tr:nth-child(even) {
background-color: #dddddd;
}
Further Resources
I've written a blogpost about the ways CSS can trip up assistive technology output: CSS Can Influence Screenreaders. For information about accessibility trees and how content is exposed to assistive technologies, check out my post The Accessibility Tree.
James Craig has a great thread on Twitter where he lays out WebKit's rationale for removing list semantics when list styles are removed, stemming from feedback provided by VoiceOver users.
If you're interested in browsers' heuristics for determining whether a minimally semantic table is a layout table or a data table, check out Chromium's source code, Firefox's source code, and WebKit's source code.
If you're interested in following me online, there's a few ways to do so:
- Follow me on Twitter
- Follow me on Mastodon
- Check out this blog or subscribe to the RSS feed
- Subscribe to Some Antics and catch past and future streams.