Icon Fonts and Screen Readers: Why Font-Based Icons Still Break Accessibility
Open DevTools on an older Shopify or WooCommerce theme’s cart icon, search glass, or hamburger menu and there’s a decent chance what renders isn’t an <svg> or an <img> at all — it’s a <span class="fa fa-cart"> or similar, styled by a @font-face icon font like Font Awesome, with the actual glyph injected through a CSS :before rule’s content property. Visually it’s indistinguishable from an SVG icon. Programmatically, it’s a completely different mechanism, and it fails in ways an SVG icon doesn’t.
Quick answer: icon fonts render their glyph through CSS generated content pulling a character from Unicode’s Private Use Area (PUA) — not through the DOM the way an <svg> or <img alt=""> does. That makes screen reader behavior unpredictable: an icon meant purely as decoration next to visible text can get announced anyway, and an icon meant to carry meaning with no text nearby can get announced as nothing, or as a nonsense character. The fix depends on which case applies: a decorative icon next to its own visible text label gets aria-hidden="true"; an icon-only control or a meaningful icon with no visible text needs role="img" and an accessible name. Both patterns come from W3C’s own documentation, not an invented convention.
This article is general accessibility information, not legal advice. Meeting WCAG 2.1 AA is a technical-practice standard, not a legal determination — consult qualified counsel for legal risk questions.
Why icon fonts are still around
SVG icons have mostly replaced icon fonts in new builds, but e-commerce themes are frequently years old and rarely get a full icon-system rewrite just because a newer pattern exists. Font Awesome, Glyphicons, and a long tail of theme-specific icon fonts still ship on live stores today — typically for the highest-traffic UI on the page: the cart icon, the search glass, the wishlist heart, the hamburger menu, the close ×, carousel arrows, and small payment/card-brand glyphs near checkout.
The mechanism is the same across all of them: a @font-face declaration loads a custom font mapping character codes — almost always from Unicode’s Private Use Area, a range the spec sets aside for custom use like this — to icon glyphs instead of letters. A CSS rule like .fa-star:before { content: "\f005"; } then injects that character visually, without it ever appearing as text content in the HTML.
The two problems this creates
The W3C WAI WCAG Working Group’s own documentation on the pattern states the core issue plainly: “For those who do use AT, voicing of icon fonts may be inaccurate, nonsensical, redundant, or unpredictable.” (Precision note: this is a WCAG Working Group wiki page, which the page itself discloses “does not necessarily represent consensus” — working-group documentation of a real, observed problem, not a numbered, normatively-adopted Technique the way the fix below is.)
In practice that unpredictability splits into two distinct failure modes, and they need two different fixes:
- A decorative icon sits next to its own visible text label — a cart glyph right before the word “Cart,” a heart icon right before “Save for later.” The icon adds nothing a screen reader user doesn’t already get from the text, but because the glyph is a real character injected via CSS, some screen reader/browser combinations announce it anyway — a stray “private use character” or an arbitrary word before the actual label, adding noise on every single icon-text pairing on the page.
- An icon conveys meaning with no visible text anywhere nearby — an icon-only cart button, a star glyph used as a “featured” badge with no caption. Here the opposite problem happens: depending on the screen reader and how the icon was marked up, the glyph may be silently skipped, read as a meaningless character, or (worst case) mispronounced as an unrelated word, since nothing in the markup tells assistive technology what the icon is for.
Fix 1: decorative icon next to visible text — aria-hidden="true"
When the icon is pure decoration and a visible text label already carries the meaning, the fix is the simplest possible one: hide the icon from the accessibility tree entirely so there’s nothing left to announce. The WCAG WG’s own worked example for this case — a heading preceded by a decorative caret icon — comes from a separate WCAG Working Group wiki page dedicated specifically to this fix (same disclosure as above: this page is also edited by WCAG WG participants and “does not necessarily represent consensus,” not a numbered Technique):
<h2>
<span class="fa fa-caret-right" aria-hidden="true"></span>
Heading preceded by a decorative icon font
</h2>
That page’s stated objective: “show how to make icon fonts that are pure decoration be ignored by assistive technology.” aria-hidden="true" on the <span> does exactly that — the glyph still renders visually, but a screen reader skips over it completely and goes straight to “Heading preceded by a decorative icon font.” No redundant character, no risk of it leaking into anything’s accessible name.
Fix 2: meaningful icon, no visible text — role="img" plus a name
When there’s no visible text doing the work — an icon-only button, or an icon used as a free-standing status indicator — hiding the icon isn’t an option, because hiding it would delete the only signal of what the control does. This is the case the officially published WCAG 2.1 Technique ARIA24 addresses directly (unlike the wiki pages above, ARIA24 is a normal numbered Technique in the published WCAG 2.1 Techniques collection, tied to SC 1.3.1 Info and Relationships).
ARIA24’s stated objective is “to show how to semantically identify an element that uses a font file for icons,” and its reasoning addresses a problem SVG icons don’t have: a low-vision user who overrides every font on a page for readability will, without this fix, have that override strip the icon font too, leaving a blank space. ARIA24’s own explanation: “The key is for the author to semantically markup icon fonts with role="img". Then the user’s font replacement selector can hook into that semantic and exclude role="img"” — a reader’s own *:not([role="img"]) { font-family: ... !important; } rule can then target everything except icons. The worked markup:
<span class="icon icon-star-bg" role="img" aria-label="Favorite"></span>
For an icon-only interactive control — a bare cart-icon button, no visible text at all — the same aria-label requirement applies, but the accessible-name target is the interactive element itself under WCAG SC 4.1.2 Name, Role, Value, not a role="img" span inside it: <button aria-label="View cart"><span class="fa fa-cart" aria-hidden="true"></span></button>. The glyph stays hidden either way; the label lives on the button — the same accessible-naming pattern this site covers in more depth in aria-label-vs-labelledby-vs-describedby-ecommerce.md and the icon-only buttons in star-ratings-review-widgets-accessible-ecommerce.md — neither of which addresses the font-specific mechanism above.
Why an automated scanner won’t catch the subtle version
axe-core’s button-name and link-name rules — “Ensure buttons have discernible text” / “Ensure links have discernible text,” both tagged to SC 4.1.2 in its own rule-descriptions.md — check whether an accessible name exists. They correctly flag a cart button with nothing: no aria-label, no text, no title. What they can’t evaluate is correctness once something is technically present: a decorative icon left un-hidden next to its own text label still has a name (the visible text), so the rule passes, even though a screen reader user now hears a stray character on every icon-text pair on the page — the same presence-versus-correctness gap this site has documented for alt text and ARIA labeling, showing up through a different mechanism here.
A quick manual test
- Turn on VoiceOver (Mac) or NVDA (Windows, free) and tab through icon-and-text pairs — a cart icon next to the word “Cart,” a heart icon next to “Save for later.” Listen for anything spoken before or after the actual label. A stray word, a beep, or “private use character” means the icon isn’t hidden.
- Tab to every icon-only button (no visible text anywhere near it — cart, search, close, hamburger). Each one should announce a clear, specific name (“View cart,” “Search,” “Close menu”) — not “button,” not silence, not the font’s raw character name.
- Check DevTools’ Accessibility pane on a decorative icon — it should show the element excluded from the accessibility tree (
aria-hidden), not listed as a generic object with no name.
FAQ
Does this apply to SVG icons too?
No — this is specific to the @font-face + CSS generated-content mechanism. An inline <svg> or <img> icon has its own, more predictable accessibility story (a <title> element, alt attribute, or aria-label directly on the element), not the PUA-character unpredictability described here.
Can I just migrate off icon fonts instead of fixing them?
That’s a reasonable longer-term fix and sidesteps the issue entirely, but it’s a theme/build change, not a quick patch — the aria-hidden/role="img" fixes above work on the icon font as-is and can usually ship without touching the icon system itself.
Get a human-reviewed audit, not just a scan
An automated scanner can confirm an icon-only button has some accessible name. It can’t listen to whether a decorative icon next to its own label is quietly adding noise on every page, or whether an icon-only control’s label actually matches what the control does. Quietramp’s audits pair an automated scan with a manual screen-reader review that catches exactly this kind of gap, delivered as a prioritized, developer-actionable PDF report. See a real sample report or check pricing — $890 one-time, $99/month for ongoing re-checks after fixes ship.
This article is general accessibility information, not legal advice. Meeting WCAG 2.1 AA does not resolve ADA or any other legal compliance question, and nothing here should be read as a compliance guarantee. Consult qualified counsel for legal risk questions.
This article was drafted with AI assistance and reviewed by a person for accuracy before publication.