Search topics
Icons and images drawn only in CSS, with no text
A plan comparison table draws its ticks and dashes as CSS background images in empty cells, so a screen reader reads every cell as blank and its user can't tell which plan includes what.
<td class="no"></td> The problem
Screen reader users can't tell which plan includes which feature, because every cell of the comparison table is read as blank.
An online shop builder compares its three plans, Starter, Shop and Pro, in a table. Each row is a feature: card payments, a custom domain, gift cards. Each cell shows a green tick if the plan includes the feature, or a grey dash if it doesn't. A sighted visitor sees at a glance that Shop adds a custom domain and only Pro has gift cards. A screen reader user moves down the Shop column and hears "Card payments, blank", "Custom domain, blank", "Gift cards, blank". Every cell in the table is read the same way. The headers are all there, and the one thing each cell is for, whether the plan includes the feature, is missing.
Each tick and dash is drawn by CSS: the cell has a class, yes or no, and the stylesheet gives each class a background image. A CSS background image has no way to carry a text alternative, and browsers don't expose it to assistive technology at all, so the cell is empty to a screen reader. That fails 1.1.1 Non-text Content: the tick is non-text content that carries information, and it has no text alternative. W3C lists this as failure F3. 1.3.1 Info and Relationships asks that information shown through presentation can be determined from the code or is available in text, so that it is kept when the presentation changes, as when a user style sheet replaces the author's. The table's structure is right, with headers on every row and column, but what each cell says is held only by a class name and a stylesheet: nothing in the HTML says "included" or "not included", and with the styles off or replaced, the answer is gone.
Browsers leave background images out when printing unless the person ticks "Background graphics" (Chrome) or "Print backgrounds" (Firefox), so a printed comparison loses its ticks too. Reader views, text-only browsers and pages with styles turned off show the empty cells in the same way.
1 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision Screen reader users hear "blank" for every cell of the comparison, so they can't compare the plans at all, or they guess from the price. Braille users meet the same empty cells.
axe and similar tools check the table's headers and pass them. They don't open the stylesheet to ask whether a background image means something, so a table of blank cells passes. ANDI warns about background images on any page that has one, as a reminder to check that they are decorative, but it can't tell which ones carry information. This audit also raises a lead for a person to check, one per table or list, wherever an empty element outside a link or button shows a CSS background or mask image or an icon font glyph: an icon-sized element, or a table cell or list item such as these. A quick AI fix may add aria-label to each td, which screen readers don't all read on a table cell, or title, which is not reliably read either. Only someone who knows the plans can say what each picture means.
Try it
Compare the table with what a screen reader reads in it.
The panel shows what a screen reader reads going down the Shop column. Does Shop come with a custom domain? With gift cards?
Broken Can you fix this?
| Starter | Shop | Pro | |
|---|---|---|---|
| Card payments | |||
| Custom domain | |||
| Gift cards |
Screen reader says:
- Card payments, blank
- Custom domain, blank
- Gift cards, blank
The fix
Give every tick and dash words in the HTML, hidden or visible, or make it an img with alt text.
- 1What a CSS image shows is also in the HTML: as words or as an img with alt.
- 2No data cell is left empty when it shows something.
- 3Keep CSS images for decoration, which can disappear without losing anything.
<table class="plans"> <thead> <tr> <td></td> <th scope="col">Starter</th> <th scope="col">Shop</th> <th scope="col">Pro</th> </tr> </thead> <tbody> <tr> <th scope="row">Card payments</th> <td class="yes"></td> <td class="yes"></td> <td class="yes"></td> </tr> <tr> <th scope="row">Custom domain</th> <td class="no"></td> <td class="yes"></td> <td class="yes"></td> </tr> <tr> <th scope="row">Gift cards</th> <td class="no"></td> <td class="no"></td> <td class="yes"></td> </tr> </tbody></table>A hidden word in each cell
<table class="plans"> <thead> <tr> <td></td> <th scope="col">Starter</th> <th scope="col">Shop</th> <th scope="col">Pro</th> </tr> </thead> <tbody> <tr> <th scope="row">Card payments</th> <td class="yes"><span class="visually-hidden">Included</span></td> <td class="yes"><span class="visually-hidden">Included</span></td> <td class="yes"><span class="visually-hidden">Included</span></td> </tr> <tr> <th scope="row">Custom domain</th> <td class="no"><span class="visually-hidden">Not included</span></td> <td class="yes"><span class="visually-hidden">Included</span></td> <td class="yes"><span class="visually-hidden">Included</span></td> </tr> <tr> <th scope="row">Gift cards</th> <td class="no"><span class="visually-hidden">Not included</span></td> <td class="no"><span class="visually-hidden">Not included</span></td> <td class="yes"><span class="visually-hidden">Included</span></td> </tr> </tbody></table>Every data cell is empty in the HTML. The ticks and dashes exist only in the stylesheet, where nothing can give them words.
The pictures stay where they were, and each cell now holds a word that says the same thing, clipped out of sight but still in the accessibility tree. A screen reader reads "Gift cards, Not included". The table looks exactly as it did; the words are in the HTML, so they are also there with styles off and are translated with the page. Like the ticks, they don't print.
All three put the information in the HTML, so all three meet 1.1.1 and 1.3.1. The hidden word keeps the design exactly and adds only the words and a visually hidden class, which most sites already have, so it suits a table drawn by a theme whose CSS you can't otherwise change; like the ticks themselves, it doesn't print. The image with alt text also keeps the look, and prints, but swaps the stylesheet's icons for images in every template that draws the table. The word in the cell changes the design, and is the only option in which sighted visitors read the answer instead of decoding a symbol; it needs a wider table, so check it at phone width. Choose by what you can change: the template, the stylesheet, or the design.
Icon fonts. An icon font draws its icons as characters inserted with ::before, usually from Unicode's private use area. Screen readers may read generated content, but for such a glyph they say nothing or an odd character, and its meaning is lost in the same way. Put the glyph on an element with aria-hidden="true", and give the cell or label its words as hidden or visible text, as in the fixes above.
Text in content. A label written only in CSS, such as content: "New" on a product card, can be read by a screen reader, as support has improved, but can't be selected, copied or found in page, and is easy to lose in a stylesheet change. Put words that carry information in the HTML.
Logos and list bullets. A logo set as the background of an empty link leaves the link with no name; that is the failure in banner and image links with no name. A list whose bullets are images that mean something (a tick for done, a cross for not started) loses that meaning the same way as the table: write it in each item.
aria-label on cells. Naming a td with aria-label looks like a shortcut, but screen readers differ in whether they read a cell's aria-label, and the words are still missing with styles off. Text inside the cell works everywhere.
Decorative backgrounds are fine. A photo behind a hero, a texture or a divider carries nothing, and CSS is the right place for it. The test is whether anything is lost when the image is gone.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Design ideas
A comparison can stay clean and still be read.
Three more ways to show what a plan includes, and two popular ones whose answer lives only in the stylesheet. The notes say what a screen reader gets from each.
In the design file
- Write the words each mark stands for next to it in the file, such as “Included” and “Not included”.
- Draw every cell or line with a mark or a value, and never leave a cell empty to mean “no”.
- Draw a missing feature with its own mark, not only greyed out or struck through.
- Draw the marks at 3:1 or more against the page, with the ratios written beside them.
Passes
Shop Pro Staff accounts 2 Unlimited Storage 5 GB 100 GB Real values instead of ticks Where a feature has an amount, the cell says it, “2” or “Unlimited”, in plain text. Nothing needs a key, and a screen reader reads each value with its row.Shop Pro Custom domain Gift cards Filled and hollow dots, named A quieter mark than a tick: a filled green dot, #1e7b34 at 5.33:1, and a hollow grey ring, #70757a at 4.65:1. Each is an image in the cell named “Included” or “Not included”.- Plan card that marks what is missing The pricing card lists every feature, with a tick or a cross before each, and a missing one in a softer grey, #6b7280 at 4.83:1. Each mark has its words, so the cross is read as “Not included”.
Fails
- Missing features greyed and struck throughWCAG 1.3.1 A popular card look, but the only sign that gift cards are missing is the grey and the line through them, both from the stylesheet. Screen readers with their usual settings read the line like the others, as if it were included.
Shop Pro Support Setup help Support level drawn as CSS starsWCAG 1.1.1 Stars cut from a mask image look crisp at any size, but the cells hold nothing, so “Support” is read with no level at all. Give the row its level in words, such as “Priority”, or make the stars one image named “3 out of 3”.
Verify the fix
4 checks, no mouse.
Test with: Screen reader, Styles off
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever the pricing table's template or its stylesheet changes.
Limits
This is a starting pattern, not a guarantee for every application. Paths, class names and the plans are placeholders. Screen readers read table cells in slightly different ways, so test with the assistive technology your users have. The same rule covers SVG and img icons used the same way; see SVG icons with no name, or read when they should be silent and emoji read out by their names, not their meaning. Test the complete journey with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.