Who it affects
Someone using a screen reader arrows across the tab bar and hears tab, 1 of 3, selected; tab, 2 of 3; tab, 3 of 3. They have to open each one to find out what it is.
Someone using voice control cannot say the tab's name to switch to it and has to fall back to showing numbered overlays.
Someone with low vision using a screen reader for support can see the icons but cannot confirm what they mean, and the screen reader cannot tell them.
Broken versus fixed
Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.
Three tidy icon tabs. A screen reader announces: tab, 1 of 3, selected; tab, 2 of 3; tab, 3 of 3. Which one is notifications? No way to tell.
Announced: Home, tab, 1 of 3, selected; Notifications, tab, 2 of 3; Settings, tab, 3 of 3. Switching tabs still needs script; the demo is static.
Show the markup behind both examples
<style>body{font-family:system-ui;padding:16px;margin:0}.tl{display:flex;gap:4px;border-bottom:1px solid #cbd5e0}.tl button{border:0;background:none;padding:8px 14px;border-bottom:2px solid transparent;cursor:pointer}.tl [aria-selected=true]{border-bottom-color:#2b6cb0}.pn{padding:12px 4px;font-size:14px}svg{display:block}</style>
<div class="tl" role="tablist">
<button role="tab" aria-selected="true" aria-controls="p1"><svg width="18" height="18" viewBox="0 0 18 18"><path d="M2 9l7-6 7 6v7H2z" fill="#2d3748"/></svg></button>
<button role="tab" aria-selected="false" tabindex="-1"><svg width="18" height="18" viewBox="0 0 18 18"><path d="M9 2a5 5 0 00-5 5v4l-2 2h14l-2-2V7a5 5 0 00-5-5zm0 15a2 2 0 002-2H7a2 2 0 002 2z" fill="#2d3748"/></svg></button>
<button role="tab" aria-selected="false" tabindex="-1"><svg width="18" height="18" viewBox="0 0 18 18"><circle cx="9" cy="9" r="3" fill="none" stroke="#2d3748" stroke-width="2"/><circle cx="9" cy="9" r="7" fill="none" stroke="#2d3748" stroke-width="2" stroke-dasharray="3 2"/></svg></button>
</div>
<div class="pn" role="tabpanel" id="p1">Welcome back. You have 2 new messages.</div>
<style>body{font-family:system-ui;padding:16px;margin:0}.tl{display:flex;gap:4px;border-bottom:1px solid #cbd5e0}.tl button{border:0;background:none;padding:8px 14px;border-bottom:2px solid transparent;cursor:pointer}.tl [aria-selected=true]{border-bottom-color:#2b6cb0}.pn{padding:12px 4px;font-size:14px}svg{display:block}</style>
<div class="tl" role="tablist" aria-label="Account">
<button role="tab" aria-selected="true" aria-controls="p1" aria-label="Home"><svg aria-hidden="true" width="18" height="18" viewBox="0 0 18 18"><path d="M2 9l7-6 7 6v7H2z" fill="#2d3748"/></svg></button>
<button role="tab" aria-selected="false" tabindex="-1" aria-label="Notifications"><svg aria-hidden="true" width="18" height="18" viewBox="0 0 18 18"><path d="M9 2a5 5 0 00-5 5v4l-2 2h14l-2-2V7a5 5 0 00-5-5zm0 15a2 2 0 002-2H7a2 2 0 002 2z" fill="#2d3748"/></svg></button>
<button role="tab" aria-selected="false" tabindex="-1" aria-label="Settings"><svg aria-hidden="true" width="18" height="18" viewBox="0 0 18 18"><circle cx="9" cy="9" r="3" fill="none" stroke="#2d3748" stroke-width="2"/><circle cx="9" cy="9" r="7" fill="none" stroke="#2d3748" stroke-width="2" stroke-dasharray="3 2"/></svg></button>
</div>
<div class="pn" role="tabpanel" id="p1" aria-labelledby="t1">Welcome back. You have 2 new messages.</div>
Test it by hand in two minutes
- Inspect each element with role="tab". It needs visible text inside it, an aria-label, an aria-labelledby, or an image with alt text.
- In Chrome DevTools, select the tab and open the Accessibility pane. The computed Name should be a word, not empty.
- Turn on VoiceOver (Cmd+F5) or NVDA, Tab to the tab bar and press the Left and Right arrows. Each tab should be announced with a name before the word tab.
- With voice control (Voice Control on a Mac, Voice Access on Windows), say click followed by the tab's name. The right tab should respond.
The fix
<button role="tab" aria-selected="true"><svg>...</svg></button>
<button role="tab" aria-selected="true" aria-label="Notifications"><svg aria-hidden="true">...</svg></button>
Name the tab for what is in its panel, in one or two words. Visible text is better than aria-label where the design allows it, because everyone benefits.
Tab with an icon image
<div role="tab"><img src="bell.svg"></div>
<div role="tab" tabindex="0"><img src="bell.svg" alt="Notifications"></div>
When the image is the only content, its alt text becomes the tab's name.
Icon tab with hidden text
<button role="tab"><svg>...</svg></button>
<button role="tab"><svg aria-hidden="true">...</svg><span class="visually-hidden">Settings</span></button>
A visually hidden span keeps the text in the page for screen readers and voice control. Use a CSS class that hides it off screen, not display: none.
Common mistakes
- Putting a title attribute on the svg and expecting it to name the tab; it rarely does.
- Naming the tab with aria-label on the tablist, which names the whole bar, not each tab.
- Using aria-label="tab 1", which is a position, not a name.
- Giving the name only on the tabpanel and leaving the tab itself blank.
- Hiding the text label with display: none, which removes it from screen readers too.
WCAG criteria this rule maps to
axe-core tags this rule as WCAG 2.2 level A. It is one of the checks for:
- 4.1.2 Name, Role, Value (level A). Official W3C text.
The scanner can see a tab whose computed name is empty. It cannot judge whether the name describes the panel it opens, so read the names next to the panels. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.
Related rules
aria-required-children
Some roles are containers that only mean something with the right children inside: listbox and option, tablist and tab, menu and menuitem, tree and treeitem, table and row.
A
ARIA roles that need a particular parent must sit inside it
aria-required-parent
Some roles only make sense inside a particular parent.
A
Buttons must have a name that says what they do
button-name
A button needs a name that assistive technology can announce.
A
SVG graphics with an image role must have alternative text
svg-img-alt
Inline SVG is drawn with code rather than loaded as a file, so there is no alt attribute to fill in.