Fixing Invalid ARIA Roles and Attributes
ARIA attributes tell assistive technology what a custom control is and what state it is in. Unlike most HTML mistakes, invalid ARIA is not quietly repaired: a misspelt attribute is simply ignored, a role the element does not allow may be dropped or announced wrongly, and a reference to a missing ID leaves the control silent.
The HTML Checker reports these as validation errors because they break the rules of ARIA in HTML. This guide works through each kind of invalid role, attribute, value and reference, with corrected markup, so every ARIA attribute on the page is one a browser actually understands.
Validity errors versus accessibility findings
There are two different questions you can ask about ARIA on a page. The first is whether the markup is valid: does each attribute exist, is each role real and allowed on that element, does each value fit the attribute, and does each reference point at something. The second is whether the result is usable: does every control have a sensible name, can a keyboard user reach it, does a screen reader announce it in a way that makes sense.
This guide deals with the first question, which is what the HTML Checker reports. Validity is the
foundation, because an invalid attribute never reaches assistive technology at all. Questions of naming,
such as icon buttons with no label or aria-label that contradicts visible text, are covered
in the ARIA labels guide in the accessibility
section.
The rules the checker applies come from two specifications. WAI-ARIA defines the roles, states and properties and the values each accepts. ARIA in HTML says which roles and attributes each HTML element may carry, for example that a heading may not be given the role of a button.
div with a role is replaced by the element it was imitating.The messages and what they mean
| Checker message (in outline) | Typical cause |
|---|---|
Bad value presentaton for attribute role | A misspelt role, an
invented role such as role="card", or an abstract role. |
Bad value button for attribute role on element h2 | A real role that ARIA in HTML does not allow on that particular element. |
Attribute aria-labeledby not allowed on element div at this point | An attribute name that does not exist, very often a spelling slip. |
Bad value yes for attribute aria-expanded | A value outside the attribute's allowed set, or an empty value. |
The aria-controls attribute must point to an element in the same document | An ID reference to an element that does not exist in the page. |
An element with role=tab must be contained in, or owned by, an element with role=tablist | A child role used without the parent role it depends on. |
Element is missing a required attribute, such as aria-checked | A widget role used without the state that role requires. |
The navigation role is unnecessary for element nav | A warning: the role repeats what the element already means. |
The exact wording varies slightly between validator versions, so search your report for the attribute or role name rather than the full sentence.
Misspelt and invented names
ARIA has a fixed vocabulary. Anything outside it is ignored by browsers, so a typo means the attribute does nothing at all. These are the slips that turn up most often:
| Written | Correct |
|---|---|
aria-labeledby | aria-labelledby (double L, even in American English) |
aria-role | role (there is no aria-role attribute) |
aria-description on older validators | Comes from the ARIA 1.3 draft; if your
checker rejects it, use aria-describedby pointing at visible or hidden text |
role="presentaton", role="dialogue" | role="presentation",
role="dialog" |
role="card", role="dropdown", role="popup" | Not
ARIA roles. Remove them, or use a real one such as group, listbox or menu
only if you implement that pattern fully |
aria-hasPopup | aria-haspopup (write ARIA attributes in lower case) |
Abstract roles
Some role names exist in the specification only to organise the others and must never be used in markup:
command, composite, input, landmark, range,
roletype, section, sectionhead, select, structure,
widget and window. role="section" is a common one, usually meant
as region, and a plain <section> element with a heading is normally the
better answer.
Roles an element is not allowed to have
ARIA in HTML lists which roles each element may take. The idea is that you should not strip an element of meaning it already has in a way that confuses users. A few patterns cause most of these errors.
A heading turned into a button
Accordions and collapsible sections often put the role on the heading itself. Headings may not take the button role, and doing so removes the heading from the page outline screen reader users navigate by. Put a real button inside the heading instead.
<!-- Invalid -->
<h3 role="button" aria-expanded="false" tabindex="0">Delivery and returns</h3>
<!-- Valid: the heading stays a heading, the button does the work -->
<h3>
<button type="button" aria-expanded="false" aria-controls="panel-delivery">
Delivery and returns
</button>
</h3>
<div id="panel-delivery" hidden>
<p>Orders placed before 2pm are dispatched the same working day.</p>
</div>
Lists given widget roles
A ul may be given roles such as menu, listbox, tablist
or toolbar, but not button or navigation. For site navigation,
wrap the list in a <nav> element rather than changing the list's role.
Images with an empty alt and a role
An image with alt="" is declared decorative. Giving it any role other than none
or presentation contradicts that, and the checker flags it. Either give it real alt text
or remove the role.
Elements that take no role
Some elements, including label, meta, script, style
and title, may not carry any role. Form controls such as input type="text"
allow very few. If you find yourself wanting to change the role of a form control, it is usually a sign
that a different control is needed.
Values, references and required structure
Values must be spelt out
ARIA states are not HTML boolean attributes. Writing aria-hidden or aria-expanded
on its own gives an empty value, which does not mean true. Each attribute has a defined set of values:
- True or false:
aria-expanded,aria-hidden,aria-disabled,aria-required,aria-selected. - True, false or mixed:
aria-checkedandaria-pressed. - A token list:
aria-currentacceptspage,step,location,date,time,trueorfalse;aria-liveacceptsoff,politeorassertive. - A number:
aria-level,aria-valuenow,aria-valuemin,aria-valuemax,aria-posinsetandaria-setsize. Text such asaria-valuenow="50%"is invalid; put the readable form inaria-valuetext.
Watch for template code that prints a variable's value directly. A JavaScript boolean printed as undefined,
or a PHP false printed as an empty string, produces an invalid value.
References must resolve
aria-labelledby, aria-describedby, aria-controls, aria-owns,
aria-activedescendant and aria-errormessage all take IDs. The first four accept
a space-separated list; the last two take a single ID. Each ID must match an element in the same document,
exactly, including case. References break when an element is renamed, when a component is rendered only
after a click, or when the referenced element sits inside a different iframe or shadow root.
Composite roles need their parts
Some roles only make sense inside a particular parent: tab inside tablist,
option inside listbox, menuitem inside menu or menubar,
row inside table, grid or treegrid, and listitem
inside list. The reverse applies too: a tablist with no tabs is incomplete.
Some widget roles also require a state; a role="checkbox" needs aria-checked,
and a role="slider" needs aria-valuenow.
<div role="tablist" aria-label="Product information">
<button type="button" role="tab" id="tab-spec" aria-selected="true" aria-controls="panel-spec">Specification</button>
<button type="button" role="tab" id="tab-care" aria-selected="false" aria-controls="panel-care" tabindex="-1">Care</button>
</div>
<div role="tabpanel" id="panel-spec" aria-labelledby="tab-spec">โฆ</div>
<div role="tabpanel" id="panel-care" aria-labelledby="tab-care" hidden>โฆ</div>
The fix, step by step
Step 1
Group the ARIA messages by type
Run
the HTML Checker on one page of each template and pull out every message that mentions role
or an attribute starting with aria-. Sort them into five piles: unknown names, roles not
allowed on the element, bad values, broken ID references and missing required structure, with redundant-role
warnings as a sixth. Most piles trace back to a small number of components, such as the menu, accordion,
tabs or carousel.
Step 2
Correct misspelt attributes and role names
Search the codebase for the exact strings the checker rejected and fix the spelling,
such as aria-labeledby to aria-labelledby and aria-role to role.
Delete invented roles like card or dropdown, and replace abstract roles such
as section or widget with a native element. Write every ARIA attribute and
role in lower case, and search templates, JavaScript and CMS content, because the same typo is often
copied into several places.
Step 3
Use only roles the element permits
Where
the checker says a role is not allowed on an element, change the element rather than forcing the role.
Put a button inside a heading instead of making the heading a button, wrap navigation lists
in a nav element, and remove roles from images with empty alt text. If a div
or span carries a widget role, consider replacing it with the native element that already
has that role and its keyboard behaviour.
Step 4
Give every state a value it accepts
Replace
bare or empty ARIA attributes with explicit values, such as aria-expanded="false" instead
of aria-expanded. Use only the allowed tokens for each attribute, numbers for range and
level attributes, and aria-valuetext for human-readable versions. Check template and script
code that outputs these values, so that booleans are written as the strings true and false rather than
as empty text, undefined or a number.
Step 5
Point ID references at real elements
For
each aria-labelledby, aria-describedby, aria-controls and similar
attribute, search the rendered page for every ID it names and confirm an element with that exact ID exists,
with matching case. Rename references after renaming elements, generate matching IDs together in reusable
components, and render the referenced element in the page from the start, hiding it with the hidden
attribute, rather than adding it only when it opens.
Step 6
Complete the structure composite roles need
Check that every child role sits inside its required parent, with no stray wrapper
element carrying a conflicting role in between: tabs in a tablist, options in a listbox, menu items in
a menu. Add the required state to widget roles, such as aria-checked on a custom checkbox
or aria-valuenow on a slider. If the component cannot support the full pattern, including
keyboard behaviour, replace it with simpler native HTML.
Step 7
Strip redundant and deprecated ARIA
Remove roles that repeat what the element already means, such as role="navigation"
on nav, role="button" on button and role="main" on
main. Replace deprecated attributes like aria-grabbed and aria-dropeffect,
and the deprecated directory role, with current patterns, such as a live region that announces
what was picked up and where it was dropped, and a plain list in place of a directory. Rerun the HTML
Checker, then test the affected components with a keyboard and a screen reader to confirm they still
announce correctly.
Where invalid ARIA comes from
- Copied snippets. Code copied from old tutorials, forum answers or AI-generated examples often uses invented roles or outdated attributes. Check each snippet against the ARIA specification before using it.
- Themes and page builders. Menus, sliders and accordions in themes frequently carry ARIA that was added once and never maintained. If you cannot edit the output, report it to the developer.
- Component libraries. Well-maintained libraries usually output valid ARIA, but wrapping their components in your own markup can break required parent and child relationships.
- Accessibility overlays. Scripts that inject ARIA into the page at runtime can add invalid or conflicting attributes. They do not appear in the server HTML, so validate the rendered page as well.
- Refactoring. Renaming an ID, splitting a component or rendering part of it later breaks references that used to work.
For content-managed sites, search the database for aria- and role= inside
post content too. Editors sometimes paste ARIA from elsewhere without knowing what it does.
Keeping ARIA valid
- Native first. Every role you do not add is one that cannot be invalid. Review new components for whether a native element would do.
- Lint the source. Linters for JSX and templates, and checkers such as html-validate on built pages, catch unknown attributes and bad values before release.
- Follow published patterns. For tabs, menus, dialogs and comboboxes, follow the WAI-ARIA Authoring Practices Guide rather than improvising the markup.
- Validate the rendered page. Check both the HTML the server sends and the page after scripts have run, because many ARIA states are set by JavaScript.
1. What happens to an ARIA attribute the browser does not recognise?
It is ignored. Browsers build the accessibility tree from attributes they know, so a misspelt attribute such as aria-labeledby, or an invented role such as card, has no effect at all. The control behaves as if the attribute were not there, which usually means a screen reader announces it without the name, state or relationship you intended. That is why these are worth fixing even though the page looks the same: the ARIA you wrote is not doing anything.
2. Is it aria-labelledby or aria-labeledby?
It is aria-labelledby, with a double L. The specification uses that spelling for this attribute even though it is otherwise written in American English, and aria-labeledby with one L is not recognised by browsers. The same double L does not apply elsewhere; the related attribute is aria-describedby. Search your codebase and content for the single-L version, because it is one of the most common ARIA errors the HTML Checker reports and it silently leaves controls without their intended label.
3. Why can't I put role=button on a heading?
ARIA in HTML restricts which roles each element may take, and headings are only allowed a few, such as none, presentation or tab. Turning a heading into a button would remove it from the list of headings screen reader users jump between, so part of the page outline disappears. The valid pattern for an accordion or collapsible section is a real button element inside the heading. It keeps the heading, and the button brings keyboard support without extra code.
4. Should I remove redundant roles like role=navigation on nav?
Yes, in most cases. The nav element already has the navigation role, so adding it again does nothing useful, and the checker reports it as unnecessary. The advice to add both dates from a time when some browsers and screen readers did not map the newer HTML elements; current browsers do. Removing redundant roles keeps markup tidy and avoids mistakes where the role and element later drift apart, for example when someone changes the element but leaves the role behind.
5. Can I write aria-hidden without a value like a normal boolean attribute?
No. ARIA states are not HTML boolean attributes. Writing aria-hidden on its own gives it an empty value, which is not the same as true, and the checker reports it as a bad value. Always write the value in full: aria-hidden equals true, aria-expanded equals false, and so on. Check template code that prints a variable directly, because an unset variable can produce an empty or undefined value that breaks the attribute without anyone noticing.
6. Why does the checker say my aria-controls must point to an element in the same document?
Because the ID it names does not exist in the page the checker received. Common causes are a typo or case difference in the ID, an element that was renamed without updating the reference, and panels or menus that are only added to the page when they are opened. Render the controlled element from the start and hide it with the hidden attribute, and generate the ID and the reference together so they cannot drift apart.
7. Are ARIA validation errors a WCAG failure?
Not automatically. WCAG 2.2 removed the old parsing criterion, so an invalid attribute on its own is no longer a failure. What matters is the effect. If a misspelt attribute leaves a button with no accessible name, or a bad role means a custom checkbox does not report whether it is checked, that fails criteria such as 4.1.2 Name, Role, Value. Fixing validation errors is the quickest way to remove those effects. This is not legal advice about any accessibility obligation.
The HTML Checker validates your markup against W3C standards and reports errors and warnings covering the doctype, encoding, tag nesting, duplicate IDs, ARIA attributes, form semantics and the language attribute, so you can fix each component at its source.
Open HTML Checker Fixes โ