Who it affects
Someone using a screen reader lands on a folder in a tree view and is not told whether it is open or closed, so they do not know whether pressing Right will expand it or move into it.
Someone relying on a live region never hears the status message because aria-live was set to a word the browser does not recognise.
Someone using a screen reader on a form hears no label on a field because aria-labelledby points at an id that was renamed.
Broken versus fixed
Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.
The arrows show Documents open and Pictures closed. A screen reader announces: Documents, tree item, 1 of 2, with no expanded or collapsed state, because yes and no are not values aria-expanded accepts.
Announced: Documents, expanded, tree item, 1 of 2, and Pictures, collapsed, tree item, 2 of 2. Opening and closing still needs script; the demo is static.
Show the markup behind both examples
<style>body{font-family:system-ui;padding:16px;margin:0;font-size:14px}ul{list-style:none;margin:0;padding:0}li{padding:3px 0}ul ul{padding-left:22px}.f::before{content:"\25BE ";color:#718096}.c::before{content:"\25B8 ";color:#718096}.d::before{content:"\2022 ";color:#a0aec0}</style>
<ul role="tree" aria-label="Folders">
<li role="treeitem" aria-expanded="yes" class="f" tabindex="0">Documents
<ul role="group">
<li role="treeitem" class="d">Invoices</li>
<li role="treeitem" class="d">Receipts</li>
</ul>
</li>
<li role="treeitem" aria-expanded="no" class="c">Pictures</li>
</ul>
<style>body{font-family:system-ui;padding:16px;margin:0;font-size:14px}ul{list-style:none;margin:0;padding:0}li{padding:3px 0}ul ul{padding-left:22px}.f::before{content:"\25BE ";color:#718096}.c::before{content:"\25B8 ";color:#718096}.d::before{content:"\2022 ";color:#a0aec0}</style>
<ul role="tree" aria-label="Folders">
<li role="treeitem" aria-expanded="true" class="f" tabindex="0">Documents
<ul role="group">
<li role="treeitem" class="d">Invoices</li>
<li role="treeitem" class="d">Receipts</li>
</ul>
</li>
<li role="treeitem" aria-expanded="false" class="c">Pictures</li>
</ul>
Test it by hand in two minutes
- Search the page source for aria- and read each value. True or false attributes (aria-expanded, aria-checked, aria-hidden, aria-pressed) must be exactly true or false, in lower case.
- For aria-labelledby, aria-describedby and aria-controls, copy each id in the value and search the page for id="that-id". Every one must exist, once.
- For list attributes (aria-live, aria-current, aria-haspopup, aria-sort), check the value is one of the words the attribute allows; polite not on, page not yes.
- Turn on VoiceOver (Cmd+F5) or NVDA and move to the element. The state or name the attribute was supposed to give should be spoken.
The fix
<li role="treeitem" aria-expanded="yes">Documents</li>
<li role="treeitem" aria-expanded="true">Documents</li>
Use the exact value the attribute defines. When your script sets it, make sure it writes the string true or false, not a JavaScript boolean that ends up as something else, and not 1 or 0.
aria-labelledby pointing at a missing id
<h2 id="billing-heading">Billing</h2>
<section aria-labelledby="billing">...</section>
<h2 id="billing-heading">Billing</h2>
<section aria-labelledby="billing-heading">...</section>
The value must be the id of an element that exists on the page. Renamed ids are the usual cause.
Live region with a made-up setting
<div aria-live="on">3 items added to basket</div>
<div aria-live="polite">3 items added to basket</div>
aria-live accepts off, polite or assertive. Anything else is treated as off.
Common mistakes
- Writing aria-expanded="expanded" or aria-checked="checked", mirroring the attribute name.
- Setting aria-hidden="yes" or aria-hidden="1" and assuming the element is hidden.
- Pointing aria-describedby at a class name instead of an id.
- Leaving aria-controls referencing a panel that is only added to the page later by script.
- Using aria-current="active"; the allowed values are page, step, location, date, time, true and false.
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 knows the value type of every ARIA attribute and checks each one, including whether referenced ids exist at scan time. It cannot tell whether a true should have been a false, so compare the state with what is on screen. 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-valid-attr
Browsers only act on attribute names that exist in the ARIA specification.
A
Required ARIA attributes must be provided for the role
aria-required-attr
Some ARIA roles only make sense with a state or value attached.
A
ids used by ARIA and labels must be unique
duplicate-id-aria
aria-labelledby, aria-describedby, aria-controls and the label for attribute all work by id.
A
ARIA attributes must follow the conditions of the element's role
aria-conditional-attr
A few ARIA attributes are allowed on a role only under conditions.