Alternative Text in the CSS content Property

Hero image for Alternative Text in the CSS content Property. Image by Mika Baumeister.
Hero image for 'Alternative Text in the CSS content Property.' Image by Mika Baumeister.

As developers, we strive (or at least we should strive) to create websites and applications that are accessible to everyone. In frontend development that's a primary focus on development in HTML, and often JavaScript components, but there are also accessibility considerations that need to be taken into account when working with CSS.

One of these aspects that I've been experimenting with over the past few weeks is how to include contextual alternative text when using the content property to insert graphical elements into the page, especially when combined with :before or :after pseudoelements to create generated content.


Why Does It Matter?

This is important because although content added to the page in this fashion should be accessible to everybody, screenreaders will generally ignore CSS content, as it is typically used for decorative purposes rather than conveying meaningful information. However, assistive technology can be configured in many different ways; during user testing for the John Lewis website we found that some configurations of JAWS would still attempt to 'read' this content.

For example, if you consider a navigation item with an arrow inserted after it, it might be coded something like this:

.link {  &:after {    content: '▶';  }}

And used in the markup like this:

<a class="link" href="/results/">  See results</a>

In our tests, we found that some assistive technology would simply announce See results whilst others would announce See results black rightpointing pointer.

Not ideal.


The Solution

Obviously, we want to offer our users a consistent and accessible experience, which is where the oftenoverlooked finer points of Alternative Text for Accessibility in the CSS3 spec come into play.

With this, we are able to pass a second field into the content property after the <contentlist>, separated using a forward slash (/):

Going back to our original example, we can use it like this:

.link {  &:before {    // may be announced as the unicode name: 'Black right-pointing pointer'    // followed by the element's inner text    content: '▶';        // may be announced as 'Alternative text', followed by the element's inner text    content: '▶' / 'Alternative text';        // the generated content will not be announced at all    content: '▶' / '';  }}

At the moment, browser support is pretty poor, but this will improve as browser development continues.

In the meantime, an alternative option would be to simply use aria attributes to pass the same contextual information to assistive technologies.


A More Scalable Approach

As anybody who's worked on even a mediumsized frontend codebase will tell you: hardcoding text into the CSS in this manner is not scalable, would be difficult to maintain, and would especially raise issues around internationalisation and localisation.

As far as I can see, there could be two different options to improve this:

Use CSS Variables

We could define the alternative texts as global CSS variables, and then pass those into the second field in content; something like:

:root {  --black-arrow-alt: 'Alternative text';}.link {  &:before {    content: '▶' / var(--black-arrow-alt);  }}

Use data* attributes

Another alternative could be to use the browser's ability to pass attribute data into the CSS via the attr function. In this way, internationalisation would only need to take place at the template level, outputting markup a little like this:

<a class="link" href="/results/" data-icon-alt="Alternative text">  See results</a>

And then access this via the CSS like this:

.link {  &:before {    content: '▶' / attr(data-icon-alt;  }}

At the time of writing, Chrome and VoiceOver don't work with the attribute approach, but as mentioned above: this will inevitably come with time.


Feature Detection

From playing around, I've found that some browsers that do not like this format within content at all, Internet Explorer in particular simply not displaying the content if a slash is included.

Usage of this can be extended by using @supports to ensure that only those browsers who are able to process it, do so:

@supports (content: 'x' / 'y') {  .link {    &:before {      content: '▶' / 'Alternative text';    }  }}@supports not (content: 'x' / 'y') {  .link {    &:before {      // We simply do not include the alternative text for      // browsers that cannot support it      content: '▶';    }  }}

Wrapping up

To summarise, there are some really cool options coming to allow us to pass more contextual alternative text, for generated content, to assistive technology. At the moment support is patchy and inconsistent, but that is changing every day.

For now, the most reliable approach is to fall back to aria attributes, but there's no harm in using feature detection to introduce these features if your user base and ecosystem allow for it.


Categories:

  1. Accessibility
  2. CSS
  3. Sass