Alternative Text for CSS Generated Content

Relying on images that come from CSS has always been risky from an accessibility perspective. CSS background images, in particular, must either be purely decorative or be described to the user in some way.

The risk is no different for images coming from CSS generated content using content: url(foo.gif) (typically paired with ::before and ::after). Addressing it is often a bit trickier.

A long time ago using CSS generated content was a WCAG auto-fail. Today using content: to insert plain text can be a boon for users — from browsers using it to present quote marks for rendering <q> to authors relying on it for hints.

Too often, however, I see developers relying on images in the generated content to convey important information to users. The following construct is not uncommon:

label.required::before {
 content: url(star.png);
}

For this to convey to a user that the field is required, the image has to load and the user has to be able to see it. If either is not the case, this can be a problem.

The Future

CSS Generated Content Module Level 3 (Editor’s Draft) allows an author to specify alternative text for images (referenced through url()) or other non-text content (glyphs). Section 1.2. Alternative Text for Accessibility offers two examples, both of which involve following the non-text content with / "text".

Using plain text with an image:

.new::before {
 content: url(./img/star.png) / "New!";
  /* or a localized attribute from the DOM: attr("data-alt") */
}

Using a blank value with a strictly decorative glyph:

.expandable::before {
 content: "\25BA" / "";
 /* a.k.a. ► */
 /* aria-expanded="false" already in DOM,
   so this pseudo-element is decorative */
}

Unfortunately support is not where we need it today to rely on it. Further, for a browser that does not support the / "text" syntax, using it will invalidate the entire declaration, preventing your image or glyph from appearing. This could be some de facto progressive enhancement (if considered carefully) or you could just repeat your code without the text alternative.

Test

I wanted to compare the current state of CSS generated content alternative text with the trusty <img alt=> construct. Granted, authors both forget and write terrible alt text all the time, so we can expect the same with CSS. But it is a good idea to get a baseline understanding.

I am using it as plain text, as a heading, and as the accessible name for each of a link, a field, and a button. I also tried it with an emoji as the text alternative to maybe head that chaos off early.

If the embed does not work, visit the pen directly or try it in debug mode for your own testing.

See the Pen JjKoeGG by Adrian Roselli (@aardrian) on CodePen.

Updated Test (10 December 2021)

Following a conversation on Twitter, I updated the text to include Apple’s proprietary alt CSS property using a fallback method. The tests below have been updated to reflect current support at the time of this writing.

Results

Takeaways

If you want to find a solution that works for each major platform, then do not use the CSS approach yet. Stick with images.

If you don’t care about iOS users, then Chrome is the only cross-platform browser that will work today, though that includes Chromium-based browsers (Edge, Opera, Brave, etc).

In supporting browsers, if your CSS generated content image does not load, its alternative text will not display. With the <img> element there is a good chance the alt will display.

CSS generated content will not localize easily. In the future, attr("data-alt") can potentially get around that unless you rely on automated translation tools. If you need your content to auto-translate, then the CSS approach is not for you.

No matter which of the two techniques you end up targeting for your users, avoid emoji as your alternative text.

Revised Takeaway (10 December 2021)

VoiceOver, Safari 15, macOS 12.0.1 Montery.

Even using Apple’s proprietary property with a fallback, with Apple’s quirky support in macOS 12 / Safari 15, Firefox’s complete lack of any support, Blink/Chromium’s other quirks, and TalkBack’s refusal to announce emoji in general, I reiterate my opening recommendation – stick with images.

Update: 25 April 2024

Chrome honors the alt text in images from CSS generated content and lets it contribute to the accName. If the image reference is broken, while the alternative text still contributes to the accName, the text does not display visually. Which means immediate 2.5.3 Label in Name failure.

Firefox still doesn’t support alternative text for CSS generated content.

Safari also honors the alt text in images from CSS generated content and lets it contribute to the accName. If the image reference is broken, while the alternative text still contributes to the accName, the text does not consistently display visually. The user may need to perform some trickery, like launching (or closing) VoiceOver. Doing so makes Safari’s <img alt> logic kick in (I am guessing here). That logic is problematic because it hides the alternative text completely if it is longer than the placeholder image or if the image is under 56 pixels in height.

A series of broken images that, when VoiceOver is activated, resize and show the alt text from the CSS declaration. Reloading the page makes them go away. Turning off VO makes them come back. The test page.

Re-revised Takeaway

Don’t rely on alternative text on CSS generated content, especially for any kind of interactive control. Stick with HTML <img>.

Martin Underhill touched on this a bit last month in Alt text for CSS generated content.

Podcast Reference (Added 7 July 2024)

This post was mentioned in the Working Draft Podcast (or at least I think it was; I don’t speak German).

YouTube: Revision 612: Neues in der Web-Plattform, Teil 2 | Working Draft.

7 Comments

Reply

Very nicely analyzed as usual, thank you!

Reply

I’m starting to see testing feedback where decorative elements added via css pseudo elements — to do things like put arrows in buttons — are being read by VoiceOver for MacOS, and then getting flagged because they’re in the reading order.

Andrew; . Permalink
In response to Andrew. Reply

Andrew, I am not sure what you mean by “flagged”, but those would not be a WCAG failure. Just a frustrating user experience. Though all the more reason not to rely on CSS generated content.

In response to Adrian Roselli. Reply

Ok – here’s what I’ve found. If an :after element contains an SVG, encoded in CSS like content: url("data:image/svg+xml [...]");, VoiceOver in Chrome on the Mac definitely reads it, and announces it as an unlabelled image, which is a critical WCAG issue.

If the :after contains a character, VO reads the character. (Not consistently, mind you — as you noted above, the performance means you should neither count on VO accessing nor ignoring pseudo-element content.)

At the moment, icon fonts that use Unicode characters that aren’t known to the browser seem to be ignored by VoiceOver, which suggests that’s the best way to add visual decoration to an element whose purpose is otherwise clear in context.

Andrew; . Permalink
In response to Andrew. Reply

Does that SVG have a any of aria-hidden="true", <title>, aria-label, or similar? If not, then yeah, that is a 1.1.1 failure. It would be a 1.1.1 failure if embedded inline as well, so that issue is more about an inaccessible SVG than a 1.3.1 issue.

I am not suggesting you said it was a 1.3.1 issue, but that was where my head was at based on the old F87 guidance.

If it has one or all those nodes/attributes and still gets read that way by VO, then that would be a VO bug.

Reply

Is there a method to indicate CSS-generated content as decorative and to be ignored by screen readers?

Aaron Farber; . Permalink
In response to Aaron Farber. Reply

The point of CSS generated content is to drop it in into the current node. So you would have to hide the entire node or create an aria-hidden node just to hold the generated content. Which seems weird.

Leave a Comment or Response

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>