Is your ARIA being thrown away?
Enter your domain. We read the HTML your server sends and check every role and aria-* attribute on the page against WAI-ARIA 1.2 — roles that do not exist, attributes nobody implements, values the attribute does not accept, labels pointing at ids that are not there — each under the criterion it fails.
Free, instant, no signup and no card. Nothing is stored.
Free, instant, no signup and no card. We read the HTML your server sends for that one page and check every role and aria-* attribute in it against the WAI-ARIA specification. Nothing is stored.
Invalid ARIA fails silently
Every year the WebAIM Million finds the same thing: home pages that use ARIA carry more detectable accessibility errors than pages that do not. Not because ARIA is bad, but because it is the one layer of a page nobody on the team can see.
A misspelled aria-hiden, a role="buton", an aria-expanded="yes" — a browser drops every one of them without a warning, in the console or anywhere else. The page renders the same, the click handlers still fire, visual QA passes. What changes is what a screen reader says: a menu button that never reports it is open, a toggle with no on or off, an icon announced as nothing.
And most of it was never written by your team. It arrives inside a component library, a cookie banner, a chat widget, a theme — so the first time many people see the ARIA their site ships is when a checker lists it.
What this checker refuses to call a failure
A free checker that a stranger runs on their own site has exactly one way to be useless, and it is to cry wolf. So six things are called failures here, all under SC 4.1.2 at level A, and each one is ARIA a browser genuinely discards or a page genuinely hidden. Everything else is reported, argued, and kept out of the count.
A role list is a fallback, not a mistake. role="switch checkbox" is valid; the browser uses the first role it knows. Only a list with no real role in it is reported. Case is ignored the way browsers ignore it, and an empty value is the default, not an error.
A missing reference is usually review. aria-controls and aria-describedby often point at a popup or tooltip that script renders on demand, so an id absent from the served HTML proves nothing. aria-labelledby is the exception, because it overrides every other name — and on a page too large to read in one pass, no reference is checked at all.
aria-labeledby and role="text" are review. Chrome and Firefox alias the one-L spelling, and WebKit's text role is used deliberately, including on Apple's own sites. Neither is in the spec; neither is broken in the way a failure count implies.
What the checker looks for
Six failures, then the review and verify items — real problems that WCAG does not forbid at level A or AA, or that cannot be proved from served markup, reported separately and never counted as violations.
- Role that does not existFailureSC 4.1.2 Name, Role, Value (Level A)
- The role attribute holds no token a browser recognises — a typo ("buton", "navigaton"), an HTML tag name used as a role ("nav", "header"), or one of the abstract roles the spec reserves for its own taxonomy ("widget", "landmark", "section"). The browser throws the whole attribute away, so the element is announced as whatever its tag is, which for a div is nothing at all. Every behaviour the author wrote the role to promise is missing.
- The fix: Use a real role from WAI-ARIA 1.2 — or better, the HTML element that already has it: `<nav>` rather than `role="navigation"`, `<button>` rather than `role="button"`. If you need a fallback, list roles space-separated, most specific first.
- ARIA attribute that does not existFailureSC 4.1.2 Name, Role, Value (Level A)
- An aria-* attribute that is not in the specification, so no browser maps it to anything and no screen reader ever hears it. It is almost always a misspelling of a real one — aria-hiden, aria-describeby, aria-role — which means the author meant to hide something, describe something or set a role, and none of it happened.
- The fix: Correct the name to the attribute that was meant; the finding shows it where the misspelling is a known one. If it is a custom data attribute, rename it to `data-*`, which is what that namespace is for.
- ARIA value the attribute does not acceptFailureSC 4.1.2 Name, Role, Value (Level A)
- The attribute is real but its value is not one it accepts: aria-expanded="yes", aria-live="loud", aria-current="active", aria-level="h2". A browser treats an invalid value as though the attribute were absent, so the state the author set — open, current, pressed, level 2 — is never announced, and a screen reader user is told a menu is collapsed when it is open, or not told which page they are on.
- The fix: Use one of the values the finding lists. For states, `true` and `false` are the words; for aria-current, `page` is the one a navigation link wants.
- aria-labelledby pointing at nothingFailureSC 4.1.2 Name, Role, Value (Level A)
- aria-labelledby names the element by pointing at other elements' ids, and it wins over every other naming method — including visible text and aria-label. When none of the ids it lists exist in the document, the name it was meant to supply is empty. A component library that generates ids at render time and a template that renamed a heading are the usual causes.
- The fix: Point it at an id that exists — check the rendered id, not the one in the source template — or remove it and use aria-label, or visible text inside the element.
- Role missing the state it needsFailureSC 4.1.2 Name, Role, Value (Level A)
- Some roles are defined by a state: a checkbox is checked or not, a slider sits at a value. An element given role="checkbox", "switch", "radio" or "slider" without aria-checked or aria-valuenow is announced as that control with no state at all, so the person using it cannot tell whether the setting is on.
- The fix: Add the state, and keep it in sync from the same code that changes the visuals — or replace the element with `<input type="checkbox">` or `<input type="range">`, which carry their state for free.
- The whole page hidden with aria-hiddenFailureSC 4.1.2 Name, Role, Value (Level A)
- aria-hidden="true" on the <body> or <html> element removes the entire page from the accessibility tree. A screen reader has nothing to read, while a keyboard user can still tab through controls nobody will describe. It is usually a modal library that hides the page while a dialog is open and was server-rendered in the open state, or never cleaned up.
- The fix: Remove aria-hidden from <body>. If it is there for a modal, put the dialog in a `<dialog>` element opened with showModal(), which makes the rest of the page inert without touching body at all.
- aria-labeledby (one L)Review
- The specified spelling is aria-labelledby, with two Ls. Chrome and Firefox accept the one-L version as an alias, so on most screen readers this works — which is why it is here rather than in the failure count. It is still not the attribute, and anything that reads the spec rather than the browser's forgiveness, including a formal audit, will report it.
- The fix: Rename it to `aria-labelledby`.
- Focusable element hidden with aria-hiddenReview
- The element carries aria-hidden="true" but is still in the tab order, which produces the worst of both: a keyboard user tabs onto something a screen reader refuses to describe, so focus lands on nothing announced at all. It sits in review rather than failure for the same reason as on the button checker — a framework can add tabindex="-1" after this reader has already run.
- The fix: If it is genuinely inactive, take it out of the tab order too — `tabindex="-1"`, `disabled`, `hidden` or `inert`. If it is active, remove aria-hidden.
- aria-label on a plain div or spanReview
- A div or span with no role is a generic element, and WAI-ARIA 1.2 prohibits naming one. Most screen readers never read the label, so whatever it was meant to say — usually the explanation for an icon — goes nowhere. It is not a WCAG failure by itself, which is why it is here, but the author plainly wanted something announced and it is not being.
- The fix: Give the element the role it actually has (`role="img"` for an icon that means something), or put the words in a visually-hidden span, which every screen reader reads.
- ARIA reference to an id not in the pageReview
- aria-describedby, aria-controls, aria-owns, aria-activedescendant or aria-errormessage points at an id that is not anywhere in the HTML the server sent. That is often fine — a dropdown or a tooltip script renders only when it opens — and sometimes the description or the error message simply never exists. Only a rendered page can say which, so it is a review item, not a failure. An aria-labelledby lands here too when its target is a React-generated id (`:r1:`, `_R_…_`), because the label may be rendered on the client.
- The fix: Open the control in a browser and check the id appears. If it never does, fix the reference; a description that points at nothing is silently dropped.
- ARIA reference to an id used more than onceReview
- An aria-labelledby, aria-describedby or similar points at an id that appears on more than one element. The browser takes the first one in the document, which is right only by accident — a product grid that renders twelve cards with the same id="title" labels all twelve buttons with the first card's title.
- The fix: Make the id unique per instance — most frameworks have a useId() for exactly this.
- Combobox or heading role without its stateReview
- role="combobox" without aria-expanded, or role="heading" without aria-level. ARIA 1.2 requires both, but a heading with no level is announced as level 2 by default and older combobox patterns omitted aria-expanded, so this is review rather than failure. The heading one is usually a level 2 that should be something else.
- The fix: Add aria-level to every role="heading" — or use an `<h1>`–`<h6>`. Add aria-expanded to the combobox and flip it when the list opens.
- role="presentation" on something focusableReview
- role="none" or "presentation" asks the browser to drop the element's semantics. On a link, a button or anything with tabindex the browser refuses, because a focusable element that announces nothing is worse than one that announces its tag — so the role is ignored. The markup says one thing and the page does another, and the next person to edit it will believe the markup.
- The fix: Remove the role. If the element really is decorative, take it out of the tab order too.
- aria-hidden with a value it does not acceptReview
- aria-hidden="yes", "1" or "hidden" is not a value the attribute accepts, so the browser ignores it and the element stays exposed. Every other invalid ARIA value on this page is a failure because it hides a state from the person who needs it; this one only fails to hide something that is already on screen, so the author's intent is lost but nobody loses content. That is why it is review.
- The fix: Use `aria-hidden="true"` if the element is decorative, or delete the attribute.
- Non-standard role="text"Review
- role="text" is a WebKit-only role that makes VoiceOver read a split-up phrase as one piece. It is not in any ARIA specification, other screen readers ignore it, and any formal audit will report it — but it is used on purpose, including on Apple's own sites, so it is reported and not failed.
- The fix: If the goal is to stop a price or a phrase being read in fragments, restructure the markup so it is one text node; otherwise leave it and accept the audit note.
- Role the element already hasVerify
- `<button role="button">`, `<nav role="navigation">`, `<main role="main">`. Harmless — the browser maps the same role either way — and not a violation. (`<ul role="list">` is deliberately excluded: it restores list semantics Safari drops when list-style is none.) It is reported because the first rule of ARIA is not to use it where HTML already does the job, and markup that repeats the implicit role is usually markup where the rest of the ARIA was written by habit too.
- The fix: Delete the role attribute. Nothing changes for anyone, and the markup is shorter.
Four rules for ARIA that works
- Use the HTML element first.
<button>,<nav>,<input type="checkbox">and<dialog>carry their role, their state and their keyboard behaviour for nothing. A role attribute carries only the promise. - Every role you add is a behaviour you owe.
role="checkbox"means aria-checked kept in sync, Space toggling it and a tabindex to reach it. If the component does not do all three, the role is a lie. - States change with the UI, or not at all. aria-expanded that is always
"false"is valid markup and a wrong answer. Set it from the same code that opens the menu. - Generate ids, then reference the generated ones. aria-labelledby written against a template id that the framework renames at render time is the most common broken reference there is. Use the framework's useId() and pass the same value to both ends.
Common questions
- What does an ARIA checker actually check?
- Validity against the specification. Every role attribute is checked against the roles WAI-ARIA 1.2 defines (plus DPUB-ARIA and Graphics ARIA); every aria-* attribute name is checked against the states and properties that exist; every value is checked against what that attribute accepts; every aria-labelledby, aria-describedby, aria-controls and similar reference is checked against the ids in the document; and roles that cannot work without a state — a checkbox without aria-checked, a slider without aria-valuenow — are checked for it. These are the questions a machine can answer from the markup alone. Whether a menu's keyboard behaviour matches its role is a question about the running page, and that is what the full scan is for.
- Why is invalid ARIA a WCAG failure?
- Because of what the browser does with it. An invalid role, a misspelled attribute or a value the attribute does not accept is simply discarded, so the role, name, state or property the author meant to expose is never exposed. SC 4.1.2 Name, Role, Value at level A requires exactly those four things to be programmatically determinable for every user interface component. The page looks identical either way, which is the whole problem: the only person who ever notices is the one using a screen reader.
- Is aria-labeledby (with one L) wrong?
- It is not the specified spelling — that is aria-labelledby, two Ls — but Chrome and Firefox both accept the one-L version as an alias, so on most screen readers it works. That is why this checker puts it in the review band rather than the failure count. Rename it anyway: anything that reads the specification rather than the browser's forgiveness, including a formal audit, will report it.
- My role attribute has two values. Is that invalid?
- No. The role attribute is a fallback list: the browser walks it left to right and uses the first role it recognises. role="switch checkbox" is valid, and this checker only reports a role attribute where no token at all is a real role. It also compares roles and values case-insensitively, because browsers do — aria-expanded="True" works and is not reported.
- It says aria-controls points at an id that is not on the page.
- That is reported for review, not as a failure, and often it is fine. A dropdown menu or a popup is frequently rendered by script only when it opens, so the id it points at does not exist in the HTML the server sent. Open the control in a browser and check the id appears. The one reference that IS a failure when missing is aria-labelledby, because the element's accessible name comes from the target and there is no other name to fall back on — aria-labelledby overrides visible text and aria-label.
- What is wrong with aria-label on a div?
- A div or span with no role is a generic element, and WAI-ARIA 1.2 prohibits giving a generic element a name. Most screen readers do not read it, so the text goes nowhere. It is not a WCAG failure on its own — nothing is lost that was otherwise exposed — which is why it is a review item. The fix is to give the element the role it actually has (role="img" for a meaningful icon) or to put the words in a visually-hidden span, which every screen reader reads.
- Why does this checker report <nav role="navigation">?
- As a verify item, never a failure. It is harmless: the browser maps the same role either way. It is shown because the first rule of ARIA is not to use it where native HTML already does the job, and markup that restates implicit roles is usually markup where the rest of the ARIA was added by habit too — which is where the real failures tend to be.
- Is aria-hidden="true" on the body really that bad?
- It removes the entire page from the accessibility tree. A screen reader has nothing to read, while a keyboard user can still tab through controls nobody will describe. It usually comes from a modal library that hides the page while a dialog is open and was server-rendered in the open state, or never cleaned up. Put the dialog in a native <dialog> element opened with showModal(), which makes the rest of the page inert without touching the body.
- It found no ARIA at all. Is that bad?
- Not necessarily. Plain, semantic HTML needs no ARIA, and a page built from real buttons, links, headings and form fields is often better off without any. It can also mean the page builds its menus, dialogs and tabs in JavaScript after load — which this reader cannot see, the same limitation as the other checkers on this site. The free scan renders each page in a real browser first, so it sees the ARIA your visitors' browsers see.
- Does this store my domain?
- No. The check is a single HTTP GET of the page you name, run inside the request and discarded when it answers. There is no account, no email field and no row written anywhere. If you go on to run the free scan, that one does keep a report so you can come back to it, which is the whole point of a report.
Keep reading
- What is your hamburger button called? →
- What do your links say out of context? →
- What does your heading outline look like? →
- Which form fields have no label? →
- Which images have no alt text? →
- The colour contrast checker →
- The free accessibility checker →
- The WCAG compliance tool →
- The ADA compliance checker →
- Which voice will read your page aloud? →
- Can a keyboard reach everything a mouse can? →