Search topics
Invalid ARIA that misleads screen readers
A checkout's custom gift-wrap checkbox writes its state as "yes" and "no", or its role as aria-role, words ARIA doesn't define, so a screen reader hears a box stuck in one state, or no checkbox at all.
aria-checked="no" The problem
Screen reader users can't tell whether gift wrap is chosen, or that the box can be ticked at all.
A shop's checkout has one extra option above the Place order button: a small square box and the words "Gift wrap this order". It isn't a real checkbox. It is a div drawn as a box, with a script that draws a tick when it is clicked or when Space is pressed on it, and ARIA to tell assistive technology what it is. The ARIA has one wrong word in it. In one version of the page, the script writes the state as aria-checked="yes" and aria-checked="no". A screen reader user tabs to the box and hears "Gift wrap this order, checkbox, checked". They don't want gift wrap, so they press Space, but it still reads as checked. The box is now ticked, and it was empty before. In another version, the role is written as aria-role="checkbox". The screen reader says only "Gift wrap this order", with no role and no state, and nothing at all when Space is pressed.
ARIA is a fixed vocabulary. Each role, each attribute and each value an attribute can take is listed in the specification, and a browser builds the accessibility tree only from the words it knows. Anything else is an author error, and the specification tells browsers what to do with it, not what the author meant:
aria-checkedtakes"true","false"or"mixed". For an unknown value of a state exposed as on or off, ARIA tells browsers to treat anything other than an empty value or"undefined"as true. Chrome does:"no"reads as checked, exactly as"yes"does, so the box is announced as checked whatever it shows.aria-roleis not an attribute at all. The browser ignores it, so thedivkeeps its own role,generic, which has no state. The correctaria-checkedbeside it is ignored too, because a generic element can't be checked.- A
rolewith no role ARIA defines, such asrole="check-box", is treated as if no role had been given, with the same result.
Each version fails 4.1.2 Name, Role, Value: the role or the state that a control must expose to assistive technology is wrong or missing, even though the source looks as though it sets both.
The first version passes a quick check: tick the box with a screen reader running in Chrome, and it says "checked". Only the empty box gives it away, and that is the state people rarely test. Other browsers can treat the same unknown value differently, so two people on the same page can hear different things.
2 of 10 groups affected
- No vision (affected)
- Low vision (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 and braille display users hear "checked" on an empty box, or hear only the words "Gift wrap this order" with no sign that they can be ticked, so they leave the wrong choice in their order or never find the option.
- Limited vision People who use a magnifier with speech see one state and hear another, and have to decide which to trust before they pay.
axe checks every ARIA word against the specification, so it reports aria-checked="no" (aria-valid-attr-value), aria-role (aria-valid-attr) and role="check-box" (aria-roles) reliably. It checks the page only in the state it finds it in, though: a script that writes "no" only after the first click passes a scan of the page as it loads. A scanner also can't tell whether a valid role is the right one, or whether aria-checked follows the tick. A quick fix can make things worse. Deleting the attribute axe names silences the report and leaves a box with no role. Renaming aria-role to role while the script still writes "yes" and "no" swaps one failure for the other. Adding aria-checked to a native checkbox "to be safe" adds an attribute browsers ignore there, and axe reports it under aria-conditional-attr as soon as it disagrees with the real state.
Try it
Tab to the gift-wrap box, then tick it and clear it with Space.
Compare the box with what a screen reader says, with it empty and with it ticked. Does the voice follow the tick?
Broken Can you fix this?
Screen reader says:
(Tab to the box)
The fix
Write every role, attribute and value exactly as ARIA defines it, or use a native checkbox and drop the ARIA.
- 1Use only words ARIA defines:
role, notaria-role;"true"and"false", not"yes"and"no". - 2Put each ARIA attribute only on a role that supports it, and never restate a native control's own state.
- 3Check the rendered markup in every state, not only as the page loads.
The state written as "yes" and "no"
<div class="tick-box" role="checkbox" aria-checked="no" tabindex="0"> Gift wrap this order</div>The ARIA written as ARIA defines it
<div class="tick-box" role="checkbox" aria-checked="false" tabindex="0"> Gift wrap this order</div>The role is right and the tick is drawn, but neither "yes" nor "no" is a value of aria-checked, so Chrome reads both as checked. An empty box is announced as checked.
role="checkbox" gives the box its role, and aria-checked holds "true" or "false", so a screen reader says "Gift wrap this order, checkbox, not checked" and then "checked" after Space or a click. The tick is drawn from aria-checked itself, so what is shown and what is heard can't drift apart. The page must render aria-checked="true" when gift wrap is already chosen, for example in a saved basket.
Both pass. The corrected ARIA fixes the component as it stands, but its role, state, Space key and saved state are all yours to keep right, and every future edit can bring an invalid word back. The native checkbox does the same job with no ARIA and no script, so there is nothing left to misspell, and the browser keeps it working. Use it. Keep the corrected ARIA for a component you can't restructure yet.
Check what is rendered, not the template. Frameworks and components turn values into attributes in their own ways. A boolean prop may render as "true", as an empty attribute, or not at all, and a component may pass through whatever string it is given. Read the attribute in the browser's inspector after each change of state.
Other invalid ARIA that axe reports. The same failure takes other forms, each with its own rule. They share one fix: use the word ARIA defines for that place, or the native element that needs none.
- A role ARIA doesn't define, or an abstract one such as
role="input"orrole="widget"(aria-roles). Use the real role, such ascheckboxornavigation, or the HTML element, such asnav. - An attribute on a role that doesn't support it, such as
aria-checkedon a button (aria-allowed-attr). A button that stays pressed takesaria-pressed: see toggle buttons that don't say whether they are pressed. Thearia-checkedleft on adivbyaria-roleis reported here too. - ARIA that restates a native control's state, such as
aria-checkedon<input type="checkbox">, reported once it disagrees with the real state (aria-conditional-attr). Delete it: the browser uses the realcheckedeither way. - A deprecated role, such as
role="directory"(aria-deprecated-role). Uselist, orul. - A braille label with no name to go with it, such as
aria-braillelabelon an element with no other name (aria-braille-equivalent, sent for review). Give the element its ordinary name first.
A misspelling that works in some browsers. Chrome and Safari also accept aria-labeledby for aria-labelledby, but Firefox doesn't, so a label can look fine in Chrome and be missing in Firefox. axe reports it under aria-valid-attr; spell it with two Ls.
A reference to nothing. An aria-labelledby or aria-describedby that points at an id not on the page is ignored, as if it were missing. axe sends it for review under aria-valid-attr-value rather than reporting it. Make sure the id exists whenever the attribute does, and write it without #.
Valid but wrong is covered elsewhere. A name on a div or span that can't hold one is in aria-label on divs and spans that screen readers skip. A state left out altogether is in toggle switches that don't say whether they are on. aria-hidden on something that can take focus is in aria-hidden content that keyboard focus still reaches, and list roles without their items are in lists built from divs, icons and typed numbers.
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Verify the fix
5 checks.
Test with: Screen reader, Keyboard, Mouse
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat whenever the component or the code that saves its state changes.
Limits
This is a starting pattern, not a guarantee for every application. Class names are placeholders, and the checkbox stands for any custom control whose ARIA is written by hand. Browsers handle author errors in their own ways and change between versions, so a page that sounds right in one browser proves little. Valid ARIA is not the same as correct ARIA: a scan that reports nothing can't tell you that a role suits the control or that its state follows the screen. 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.
References
- WCAG Understanding: name-role-value (opens in a new tab)
- w3.org: wai-aria-1.2 (opens in a new tab)
- w3.org: wai-aria-1.2 (opens in a new tab)
- w3.org: using-aria (opens in a new tab)
- developer.mozilla.org: checkbox_role (opens in a new tab)
- dequeuniversity.com: aria-valid-attr-value (opens in a new tab)
- dequeuniversity.com: aria-valid-attr (opens in a new tab)
- dequeuniversity.com: aria-roles (opens in a new tab)
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.