Your icons are perfectly legible. What do they announce?
The three icons in your header are a magnifier, a bell and a cart. Anybody looking at them knows exactly what they do. A screen reader reaching them says “button. button. button.” — and no alt-text checker in the world will tell you, because alt is not a valid attribute on <svg> and never has been. Paste a page and we will sort every graphic on it into the four states a screen reader can actually be in about it.
Free, instant, no signup and no card. We read the HTML your server sends, and nothing is stored.
Try the page with the most icons on it — a header, a dashboard or a pricing page. Free, instant, no signup and no card, and nothing is stored.
The failure your image audit is structurally unable to see
Every accessibility tool built around images reads <img> and looks for alt. The icons on a modern site are not <img>. They are inline SVG, pasted into the markup by an icon component, and they cannot take an alt attribute at all. So a page with thirty unnamed icons comes back from an alt audit as a page with no images on it: a clean pass, on the exact defect that clean pass was supposed to rule out.
An SVG gets its name from one of three places instead — aria-label, aria-labelledby, or a <title> child element — and that third one is not an attribute, which is why attribute-shaped checkers miss it. On top of that, <svg> has no default role, so a graphic that says nothing about itself is not safely ignored: it is handled differently by every browser and reader combination.
The four states, and where yours land
Every graphic on a page is in exactly one of these, and on screen all four look identical — which is the whole reason nobody has ever audited their own icon set.
NAMED aria-label, a resolving aria-labelledby, or <title>
→ announced. Correct for a chart, a logo, a diagram.
HIDDEN aria-hidden="true", role="presentation", or sitting
inside a control that already names itself
→ deliberately silent. Correct for an icon beside a word.
AMBIGUOUS no role, no aria-hidden, no name
→ the browser decides. Ignored, or "image", or an empty
graphics-document VoiceOver can navigate into.
BROKEN an icon-only button with nothing to announce, a
role="img" with no alternative, an aria-labelledby
pointing at an id that is not on the page
→ promises a name and delivers none. Level A.Six real pages, read live on 27 September 2026
Run against real home and sign-in pages before this page existed. The spread is the argument for a ledger rather than a score: these sites are not bad at accessibility, and they still land in four different places.
stripe.com 150 graphics — 44 named, 105 hidden, 1 undeclared
tailwindcss.com 81 graphics — 33 named, 31 hidden, 17 undeclared
vercel.com 46 graphics — 22 named, 20 hidden, 4 undeclared
css-tricks.com 41 graphics — 36 of them inside an aria-hidden
subtree nothing can reach at all
github.com/login 6 graphics — all 6 hidden; the one "named" icon
turned out to be inside a <div hidden> banner
news.ycombinator.com 0 inline SVG — and the logo is an <img src="y18.svg">
with no alt attribute at allThe last two are there because they broke this tool before they were fixed in it. The GitHub read was reporting a clean pass off a graphic sitting in a hidden flash banner; the Hacker News read was headlining “no inline SVG on this page” over a real missing-alt finding. Both are now pinned by fixtures, which is the only reason the numbers above are worth anything.
What we refuse to call a failure
The expensive mistake a checker can make is telling a stranger their correct page is broken, so five patterns are never reported however aggressive that makes the tool look:
- An aria-hidden icon beside a word. That is the recommended technique, not a missing alternative.
- An unmarked icon inside a control that names itself. The button owns the name; the graphic is decoration.
- role="presentation" and role="none". A deliberate decorative declaration, the same as aria-hidden.
- alt="" on an .svg file. An empty alt is a statement, not an omission.
- A control that is itself aria-hidden. It is not announced as an anonymous button — it is not announced at all, and that is a different finding.
Everything this checker reports
Three things it counts as a failure, eight it reports without counting against you, and an honest branch for a page where the answer cannot be read from HTML.
Icon-only button or link with no name anywhere
Announced as nothingSC 4.1.2 Name, Role, Value (Level A)
This control contains an SVG and nothing else — no text, no aria-label on the control, and no name on the graphic either. Somebody looking at it sees a hamburger, a magnifier, an X or a cart and knows exactly what it does. A screen reader reaches it and says "button", or reads the URL, and that is the whole announcement. Where a header has three of them in a row, they are three identical, unnameable controls, and the person has to activate one to find out what it was. This is the single most common icon defect on the web and it is the one a plaintiff's demo video opens with.
The fix: Put the name on the CONTROL, not the graphic: <button aria-label="Search"> and leave the SVG inside it aria-hidden="true". One name, in the place the accessibility tree looks first, and no chance of the icon and the button announcing two different things.
SVG declared as an image with no text alternative
Announced as nothingSC 1.1.1 Non-text Content (Level A)
The graphic carries role="img" (or a graphics-* role), which tells assistive technology it is meaningful content rather than decoration — and then gives it no name: no aria-label, no aria-labelledby that resolves, and no <title> child with text in it. That combination is worse than saying nothing at all, because the reader has been promised an image and handed an empty string. Most readers announce "image" or "graphic" and stop. This is the SVG form of an <img> with no alt, and it is what axe reports as svg-img-alt and role-img-alt.
The fix: Add aria-label="What the graphic conveys" to the <svg>, or a <title> as its first child element. If the graphic is in fact decorative, the fix is the other direction: drop role="img" and add aria-hidden="true".
aria-labelledby pointing at an id that is not on the page
Announced as nothingSC 1.1.1 Non-text Content (Level A)
The SVG names its label by id and no element with that id exists in the served HTML. The reference resolves to nothing, so the graphic has, in effect, no name — and it looks completely correct in the source, which is why this one survives review. In inline SVG it has a specific and very common cause: an icon component that emits <title id="icon-title"> is pasted onto the page five times, the ids collide, and the sixth copy's label resolves to the first copy's text or to nothing at all.
The fix: Make the id unique per instance, or stop using the indirection: aria-label on the <svg> needs no id and cannot break. If the label really is elsewhere on the page, confirm that element ships with the page rather than being rendered later.
SVG that says nothing about whether it is content or decoration
Not a failure — worth readingSC 1.1.1 Non-text Content (Level A)
These graphics have no role, no aria-hidden, no aria-label and no <title>, and they are not inside a control that already carries a name. <svg> has no default role, so what happens next depends on the browser and the reader: some ignore it, some announce "image", and VoiceOver in Safari exposes it as a graphics-document that can be navigated into and read as an empty group. Most of them are almost certainly decorative — the problem is that nothing in the markup says so, so the decision is made differently by every combination of software.
The fix: Decide, in the markup, once. Decorative: aria-hidden="true" on the <svg> (and focusable="false" if you still support legacy Edge). Meaningful: role="img" plus aria-label. It is one attribute either way and it removes the graphic from three different browsers' guesswork.
Accessible name that came out of the design tool, not out of a sentence
Not a failure — worth readingSC 1.1.1 Non-text Content (Level A)
The graphic's <title> or aria-label is a name the export produced — "Layer 1", "Group 4", "Artboard", "Vector", "Combined Shape", "Untitled", "icon". It satisfies every automated check that counts the presence of a name, and what it announces is a fact about a file, not about the page. Somebody hears "Layer 1, image" and has learned nothing except that there is a graphic here and that nobody looked at it. It is reported as review rather than a failure because a human has to judge the wording, but in practice this name is never the right one.
The fix: Write what the graphic means in the sentence it is sitting in — "Downloads rose 40% in March" rather than "Chart". If it means nothing, that is the better answer: delete the title and add aria-hidden="true".
The same icon pasted in several times, carrying the same id each time
Not a failure — worth readingSC 4.1.2 Name, Role, Value (Level A)
Two or more <title> elements inside SVGs on this page share an id. Ids have to be unique in a document, and every aria-labelledby pointing at that id resolves to the FIRST one — so the fourth copy of an icon component announces the first copy's name. When the copies are the same icon it is harmless; when a component was duplicated and re-labelled, every instance says whatever the first one said.
The fix: Generate the id per instance (a counter, a React useId, the record's own id) or drop the aria-labelledby indirection entirely and put aria-label on each <svg>.
Two different names on one graphic, and only one of them is read
Not a failure — worth readingSC 1.1.1 Non-text Content (Level A)
The SVG carries both an aria-label and a <title>, and the two say different things. Only the aria-label is announced — it wins the accessible-name calculation outright — so the <title> is dead text that still appears as a browser tooltip on hover. That split is how a graphic ends up saying one thing to a sighted mouse user and another to a screen reader, and it usually means one of the two was updated and the other was not.
The fix: Keep one. aria-label if the name is short and the tooltip is unwanted; <title> alone if you want the hover text to match what is announced.
<title> buried inside the SVG instead of opening it
Not a failure — worth readingSC 1.1.1 Non-text Content (Level A)
The SVG has a <title> and it is not the first child element — usually it has been pushed down by a <defs>, a <style> or a <g> that an export wrote first. Modern browsers still compute the name from it, but the SVG specification asks for it as the first child, older readers only honour it there, and a <title> nested inside a <g> names that group rather than the graphic. It is also the position that survives somebody editing the file later.
The fix: Move <title> to be the first child of <svg>, above <defs>. If you want the name to be unmissable, add aria-label to the <svg> as well — it takes precedence and does not depend on position.
Name written on a graphic that is hidden from the people who would hear it
Not a failure — worth readingThe SVG carries aria-hidden="true" and also has a <title> or an aria-label. aria-hidden removes the element and everything inside it from the accessibility tree, so the name is never announced — somebody wrote it and nobody will ever hear it. This is not a conformance failure, and it is worth knowing about, because it is the signature of an icon whose author believed it was meaningful while whoever wired it up marked it decorative.
The fix: Settle which it is. Decorative: delete the <title>, keep aria-hidden. Meaningful: remove aria-hidden and add role="img" — unless it sits inside a control, in which case the name belongs on the control and the icon should stay hidden.
Graphic that the Tab key stops on for no reason
Not a failure — worth readingSC 2.4.3 Focus Order (Level A)
The SVG carries focusable="true" or a tabindex of 0 or more, which puts a non-interactive graphic into the tab order. Somebody navigating by keyboard presses Tab and lands on a picture: nothing is announced, nothing can be activated, and on a page with an icon in every row that is one dead stop per row between them and the thing they were reaching for. The usual cause is a legacy focusable attribute copied forward, or a tabindex added to make an SVG scrollable.
The fix: Remove tabindex from the graphic, and set focusable="false" rather than "true". If the graphic genuinely is the control, make the control a <button> and put the SVG inside it.
SVG loaded through <img> with no alt attribute
Not a failure — worth readingSC 1.1.1 Non-text Content (Level A)
The page points an <img> at an .svg file and gives it no alt attribute at all. That is an ordinary missing-alt failure rather than an inline-SVG one, and a screen reader falls back to announcing the file name — "logo-final-v2-svg, image". It is listed here because a page full of inline icons usually has a couple of these too, and because the fix is different: an <img> takes alt, an inline <svg> cannot. Note that alt="" is NOT reported — an empty alt is a deliberate statement that the image is decorative, and it is correct.
The fix: Add alt="…" describing what it conveys, or alt="" if it is decorative. The image alt checker reads every <img> on the page the same way.
No inline SVG on this page
Verify by handThere is no <svg> element in the served HTML. Icons can arrive several other ways — an icon font, a CSS background, a sprite fetched at runtime, an <img> pointing at an .svg file — and none of those are inline SVG, so this checker has nothing to read. An icon font is worth checking by hand, because a glyph in a ::before is announced as whatever private-use character it maps to, which is usually nonsense.
The fix: If the icons on this page come from a font or a background image, run the site-wide scan: it reads the rendered page and can see what those actually announce.
Questions
- Why doesn't my alt text checker find any of this?
- Because alt is not a valid attribute on <svg>, and never has been. An alt-text audit reads <img> elements, so a page whose thirty icons are all inline SVG is reported as having no images on it at all — a clean pass, on a page where none of the icons are named. The accessible name of an SVG comes from aria-label, from aria-labelledby, or from a <title> CHILD ELEMENT, and that last one is not an attribute at all, so an attribute-shaped checker cannot see it.
- What actually happens when a screen reader meets an unnamed icon button?
- It announces the role and nothing else: "button". If the control is a link it may fall back to reading the URL, so a cart icon becomes "link, slash cart". Where a header holds three icon buttons in a row, they are three consecutive announcements of the word "button", indistinguishable from one another, and the only way to learn what any of them does is to activate it and see what happened. That is SC 4.1.2 Name, Role, Value at Level A.
- Should the name go on the icon or on the button?
- On the button, every time. <button aria-label="Search"><svg aria-hidden="true">…</svg></button> is the pattern to reach for: one name, in the place the accessibility tree looks first, and no possibility of the graphic and the control announcing two different things. Putting the name on the SVG instead does usually work, because a control with no name of its own computes one from its contents — but it breaks the moment somebody adds a second icon, and it means the name lives in your icon component rather than at the call site where the button's purpose is actually known.
- Is aria-hidden on an icon a bad thing?
- No — it is the recommended technique for decoration, and this checker never reports it as a missing name. An icon sitting beside the word it illustrates should be aria-hidden="true", because otherwise the screen reader announces the concept twice. What the tool does report is the combination: aria-hidden on the graphic AND no name anywhere on the control it is the only content of. Each half is fine; together they produce a button that says nothing.
- You flagged icons that have no role and no aria-hidden. Aren't those just decorative?
- Most of them are, and that is why they sit in the review band rather than the failure band. The reason they are worth reporting at all is that <svg> has no default role, so what happens is decided by software rather than by you: some readers ignore it, some announce "image", and VoiceOver in Safari exposes it as a graphics-document a user can navigate into and hear as an empty group. One attribute settles it for every combination. Measured live on 27 September 2026, tailwindcss.com left 17 of its 81 graphics in this state and stripe.com left 1 of 150.
- My SVGs have a <title> in them. Isn't that enough?
- Usually yes, with three caveats this tool checks. The <title> must be the first child element of the <svg> — an export that writes <defs> first pushes it down, and older readers only honour it in first position. It must say something: "Layer 1", "Group 4", "Vector" and "Artboard" all satisfy a presence check and announce a fact about a file. And if the same icon component is pasted onto the page five times, five <title id="icon-title"> elements now share one id, and every aria-labelledby pointing at it resolves to the first one.
- What about icon fonts and CSS background images?
- This checker reads inline <svg> and reports honestly when a page has none. Icon fonts are worth checking by hand and they fail differently: a glyph in a ::before pseudo-element is announced as whatever private-use Unicode character it maps to, which on most readers is silence and on some is a garbled字 read aloud. A CSS background image has no accessible name mechanism at all. Both of those need a rendered page, which is what the site-wide scan does.
- Why is a link that is itself aria-hidden not reported?
- Because it is not announced as an anonymous button — it is not announced at all. aria-hidden on an element removes it and everything inside it from the accessibility tree. That IS worth knowing about when the element is still focusable, because a keyboard user can land on something no screen reader will describe, but it is a different defect with a different fix and the keyboard checker is where it belongs. A live read of css-tricks.com on 27 September 2026 found 36 of its 41 graphics inside exactly this kind of subtree; reporting them here as Level A failures would have been 32 false accusations on a page that has none.
- Does this replace a real audit of my graphics?
- No. It reads the wiring, not the wording — whether a name exists and resolves, not whether it is any use. "Chart" is perfectly wired and tells nobody anything; the right name for a graph is usually the sentence the graph is making. It also reads one page, and it cannot see icons that only exist after JavaScript runs. The site-wide scan renders every page in a real browser, which is where the rest of that lives.
- Which criteria is this checking?
- Two, mostly. SC 1.1.1 Non-text Content (Level A): all non-text content has a text alternative serving the equivalent purpose, or is marked so it can be ignored by assistive technology — note that the second half is a conforming answer, which is why declaring an icon decorative counts as getting it right. SC 4.1.2 Name, Role, Value (Level A): for every user-interface component, the name and role can be programmatically determined. The stray-tab-stop finding is SC 2.4.3 Focus Order.
The rest of what a graphic owes somebody
A photograph with no alt text fails differently — the image alt text checker reads every <img> on a page. A control that carries a word rather than an icon is read by the button checker, a graphic that a keyboard lands on for no reason is the keyboard checker’s business, and because icons ship from a shared component, the full WCAG scan is what tells you which of your pages the component never reached.