Let's say we're building a shopping list app. As we build out the app, we decide to style the list, stripping out the bullets that the browser gives us by default.
<ul style="list-style: none;">
Being dutiful accessibility testers, let's run our screenreaders over the two lists. Pause for a moment and ask yourself: do you expect any difference between how the two lists are announced? Why or why not?
I was able to test the two lists with NVDA for Windows and VoiceOver for macOS. I ran NVDA against the lists on Chrome, Firefox, and even Internet Explorer. I ran VoiceOver against Chrome and Safari. Here's what I found:
When I tested against the first, bulleted list, the screenreaders always told me how many items were in the list and preluded each list item with "bullet."
When I tested against the second, bulletless list, the screenreaders never said "bullet."
Most surprisingly, Safari with VoiceOver didn't treat the bulletless list as a list at all, omitting any announcements about how many items were in the list.
As we keep building our hypothetical shopping list app, we implement a feature to let users add new items, complete with a shiny new "Add" button. We'll even set it to be all uppercase with CSS.
<button style="text-transform: uppercase;">
Upon testing the page with screenreaders, our screenreader's readout confirms that it is receiving the "ADD" text in all caps. Usually, it's totally fine for a screenreader to receive a word in all caps—they're usually smart enough to realize it's just a capitalized word. If you navigate to the above button with VoiceOver, however, you'll learn that VoiceOver has confused the capitalized "ADD" button for the acronym A.D.D.—something it definitely wouldn't have done if we hadn't changed the CSS.
These cases of CSS messing with our screenreader announcements are initially shocking, perplexing, and maybe even appalling. After all, they seem to conflict with our mental model of CSS, one that's likely been instilled in us since we started learning web development: HTML is for content, and CSS is for visual appearance. It's the separation of content and presentation. Here, by changing what screenreaders announce, it feels like CSS is encroaching on content territory.
What is happening here? Do we need to worry about every CSS rule changing screenreader announcements?
Screenreaders aren't actually looking at the CSS.
Browsers package up an alternate version of the DOM, called the accessibility tree, which it passes to the user's operating system for screenreaders and other assistive technology to consume. Every element in the tree is defined as a set of properties that describe the element's purpose and functionality. Screenreaders peruse the tree to know what to announce. Thanks to the hard work of browser engineers, browsers have gotten really smart about building the tree. They can account for web developers' tricks—whether best practices or bad habits—and curate a more usable accessibility tree.
As much as the web development community talks about the separation of content and presentation, the truth is that it's not that easy. Between using pseudo-elements and toggling
display: none; on elements to show or hide them, it's clear there can be a bit of a gray area between content and its presentation. This gray area provides a key space for browsers to optimize their accessibility trees, giving all screenreader users the same experience of the content as sighted users.
CSS's Potential Influences on Screenreaders#
What kinds of CSS-based optimizations or modifications do browsers make to the accessibility tree? Below, I've listed a few kinds that I know of. I'm sure it's not exhaustive. More importantly, these impacts will depend heavily on the user's choice of operating system, browser, and assistive technology. On the WebAIM blog, John Northup cautions us,
"It’s tempting to assert that if you do x, 'the screen reader' will announce y. Sometimes it really is just that simple, but in a surprising number of situations, it just isn’t that absolute."John Northup, WebAIM, Screen Readers and CSS: Are We Going Out of Style (and into Content)?
Be sure to test each of the following on many different browsers and with many different screenreaders.
The clearest instance of CSS-as-content is pseudo-elements, which can inject content into the page without adding it to the DOM. For instance, Firefox and Safari both support the
::marker pseudo-element, which injects a bullet point, number, or other indicator before a list item.
We can also use the
::after pseudo-elements to inject content.
content: "✏️ ";
If you navigate to the above button with a screenreader, you'll likely hear something like "button, pencil, Edit," assuming your screenreader supports emojis. Lately, browsers interpret the content defined in pseudo-elements as... content. It impacts how sighted users experience the page (and users don't really care whether their content is real content or pseudo content), so browsers judge that they need to expose it to screenreaders.
This judgment call comes from the W3C specs for determining an element's accessible name, i.e. how it's announced by screenreaders:
Check for CSS generated textual content associated with the current node and include it in the accumulated text. The CSS
:afterpseudo elements can provide textual content for elements that have a content model.
Accessible Name and Description Computation 1.1, Step 2(F)(ii)
:beforepseudo elements, User agents MUST prepend CSS textual content, without a space, to the textual content of the current node.
:afterpseudo elements, User agents MUST append CSS textual content, without a space, to the textual content of the current node.
Sometimes, we find ourselves wanting to hide something visually, but still expose it to screenreaders, usually to provide a hint for context that would be obvious visually. In these cases, it's tempting to specify
display: none;. That would hide the contents, but still leave them in the DOM. Mission accomplished, right?
display: none; is generally used as a toggle, to save the trouble of recreating and reinserting content on command. For instance, you could use
display: none; for inactive tab panels or for whichever slides the carousel is not showing at the moment. When
display: none; is applied to an element, the assumption is generally that users will not be able to experience that element, and often that it would be confusing or misleading for them to.
display: none;, as well as similar rules such as
visibility: hidden; and
width: 0px; height: 0px;, as cues that the elements aren't meant to be read by anyone, and will remove the relevant elements from the accessibility tree accordingly. This is why we resort to tricks such as placing the elements far off screen or clipping the elements to be really small to expose information to screenreader users only.
When a user reaches a list in Safari, VoiceOver will usually say something like "list, 2 items." When the user navigates between items, VoiceOver tells them where they are in the list, e.g. "1 of 2." However, as we saw earlier, applying
list-style: none; to the list changed the user's experience entirely. VoiceOver no longer said "list, 2 items," nor did it tell the user how far into the list they were. Instead, it just treated every item as a plain text node. It seems as though Safari's engineers decided lists without bullets or other markers aren't listy enough, and decided to instead nullify the list's semantics. Alternatively, it could be a bug.
Tables are another area where CSS can lead to changed semantics. Even though tables are supposed to represent tabular data, developers used to (and still sometimes do) put pieces of the page into a table in order to define the layout in terms of rows and columns. In these cases, table semantics are inaccurate.
Browsers like Chrome and Firefox will make an educated guess at whether a table is used for layout. One factor they consider is tablelike styling such as zebra striping. On the other hand, specifying
display: block|flex|grid on a table element seems to be an instant disqualifier for tablehood, and causes browsers to blow away the table's semantics.
display loves changing if and how elements are announced by screenreaders...
An Obligatory Mention of the CSS Speech Module#
It doesn't quite fit into this post's theme of browsers optimizing accessibility trees, but a post about how CSS can influence screenreaders would be remiss without a mention of CSS Speech.
CSS2 contained specs for aural stylesheets, which could define speech synthesis properties for screenreaders and any other device that would read a webpage aloud. These properties included volume, pitch, family (i.e. which voice was even used), audio cues that could be announced before and after elements, and more. This was replaced by the
speech media type in CSS 2.1, which had no defined properties or values. It was just a reserved keyword.
In 2012, W3C released the CSS Speech Module for CSS3 as a Candidate Recommendation to get implementation experience and feedback before formally recommending it. The module was fairly similar to the old aural stylesheets of CSS2, with some additions. For instance, the new
speak-as property dictated how verbose speech synthesizers would be when reading out an element—e.g. spelling out every letter, reading digits individually, or announcing every punctuation mark. Additionally, we could distinguish regular content and live alerts with different voices. However, the module received limited support, and was retired in 2018.
As of February 2020, it seems like the CSS Speech Module might be making a comeback with a new Candidate Recommendation. If this recommendation sees a more widespread adoption, we can expect to use CSS to influence screenreaders even more.
With CSS, there is a gray area, for better or for worse, between content and its presentation. When CSS bleeds into content, it can convey important information that might be lost to screenreader users. Browsers have gotten really smart about how they expose that information to screenreaders, but that means that our styles can change screenreader users' experience in unexpected ways. As always, be sure to test with many screenreaders on many browsers and many platforms.
Can I use..., CSS ::marker pseudo-element | ↩︎
Chromium source code | ↩︎
Firefox source code | ↩︎
Steve Faulkner, The Paciello Group, Short note on what CSS display properties do to table semantics | ↩︎
Adrian Roselli, Tables, CSS Display Properties, and ARIA | ↩︎
David Jöch, The CSS Speech Module | ↩︎
CSS Speech Module Publication History | ↩︎