I have run formal accessibility audits on over 80 websites and web applications since founding QA11Y Labs. Before that, I spent decades navigating the web with a screen reader, which means I have personally experienced every broken ARIA pattern that exists.
There are five ARIA mistakes I find on almost every site I audit. Not occasionally — almost every site. These are not edge cases. They are the default failures that happen when developers understand what ARIA attributes do in isolation but not how they interact with the accessibility tree, with screen reader modes, or with user expectations.
Mistake 1: Using role to Change Semantics Without Changing Behavior
The most fundamental ARIA mistake is treating role as purely a semantic label without understanding that roles carry implicit behavior expectations. When you give an element a role, you are making a promise to the screen reader user about how that element will behave.
The Classic Example
<!-- Developer wants a "checkbox" that looks different -->
<div
role="checkbox"
aria-checked="false"
class="custom-check"
onclick="toggleCheck(this)"
>
Accept terms
</div>
This tells NVDA and JAWS: "This is a checkbox." Both screen readers will announce it as a checkbox. Both will tell the user it is in a checked or unchecked state. Both will allow the user to navigate to it with the X shortcut key (which cycles through checkboxes in browse mode).
But when a screen reader user presses Space to toggle the checkbox — because that is the standard keyboard interaction for checkboxes, and every screen reader user knows this — nothing happens. The div has no keyboard event listener. The click handler only fires on mouse clicks.
The fix is not just adding tabindex="0" and a keydown listener for Space. The fix is using a real <input type="checkbox"> and styling it. Every custom interactive widget built with ARIA requires you to implement the entire keyboard interaction pattern from the ARIA Authoring Practices Guide. Most developers do not.
<!-- Correct: real checkbox with custom visual styling -->
<label class="custom-check-wrapper">
<input type="checkbox" class="sr-only" />
<span class="custom-check-visual" aria-hidden="true"></span>
Accept terms
</label>
Mistake 2: aria-label That Duplicates or Contradicts Visible Text
I see this constantly in audits. A button has visible text that says "Learn more" and an aria-label of "Learn more about our enterprise pricing plan." The intent is good — adding context so screen reader users know what "Learn more" refers to. The implementation breaks things in ways the developer did not anticipate.
What Actually Happens
When aria-label is present on an element, it completely overrides the element's text content in the accessibility tree. Screen readers announce the aria-label value instead of the visible text. So a sighted user and a screen reader user see/hear different things. Voice control users who use speech recognition software like Dragon NaturallySpeaking say "click Learn more" — and the control does not activate, because the accessible name is "Learn more about our enterprise pricing plan," not "Learn more."
<!-- Problematic: aria-label overrides visible text -->
<button aria-label="Learn more about our enterprise pricing plan">
Learn more
</button>
<!-- Better: make the visible text descriptive -->
<button>
Learn more <span class="sr-only">about our enterprise pricing plan</span>
</button>
<!-- Or: use aria-describedby for supplementary context -->
<button aria-describedby="pricing-desc">Learn more</button>
<p id="pricing-desc" class="sr-only">About our enterprise pricing plan</p>
The rule of thumb I use: if you need aria-label on an element with visible text content, that is usually a signal that the visible text needs to be improved. Make the text more descriptive, and use visually hidden text extensions rather than overriding the accessible name entirely.
Mistake 3: Using aria-hidden="true" in the Wrong Direction
The correct use of aria-hidden="true" is to remove purely decorative elements from the accessibility tree — icons, background images, purely presentational divs. The incorrect use is to hide content that screen reader users need.
The Overuse Pattern
I find two distinct patterns of aria-hidden misuse in audits. The first is hiding custom interactive components because the developer knows the component is broken and does not want screen reader users to reach it. This is not a fix — it is hiding a barrier. The component needs to be fixed.
The second pattern is accidentally hiding content that appears multiple times on a page. A developer decides that navigation landmarks are cluttering the screen reader experience and adds aria-hidden="true" to the secondary nav. But now keyboard users who navigate by Tab can still reach those links — they just hear nothing when they land on them.
<!-- Dangerous: aria-hidden on a focusable container -->
<nav aria-hidden="true">
<a href="/page">Link</a> <!-- Still focusable! Now announces nothing. -->
</nav>
<!-- Correct: if hiding decorative icons only -->
<button>
<svg aria-hidden="true" focusable="false">...</svg>
<span>Submit</span>
</button>
The critical rule: never apply aria-hidden="true" to an element that contains focusable children. If something can receive focus via Tab, it must have an accessible name. Hidden and focusable is one of the worst screen reader experiences possible — you navigate somewhere and hear nothing.
Mistake 4: Landmark Region Overuse and Misuse
Landmark roles — main, nav, aside, header, footer, search, form — are supposed to be the skeleton of the page. Screen reader users navigate by landmarks to skip to the content they want. I use this every day. When landmarks are misused, they become noise instead of signal.
Too Many <nav> Elements
Every <nav> element without a distinct aria-label is announced as "navigation." A page with five unlabeled nav elements means a screen reader user hears "navigation navigation navigation navigation navigation" when cycling through landmarks. None of them are distinguishable.
<!-- Unlabeled: impossible to distinguish -->
<nav>...Primary menu...</nav>
<nav>...Breadcrumbs...</nav>
<nav>...Pagination...</nav>
<!-- Correctly labeled -->
<nav aria-label="Primary">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>
Landmark Elements in the Wrong Places
A <header> element inside an <article> is not a landmark — it is scoped to the article. A <footer> inside a <section> is not a landmark. Only top-level <header> and <footer> elements (not descendants of article, section, aside, main, or nav) map to the banner and contentinfo roles. Developers often expect landmark behavior that the HTML spec does not deliver.
Mistake 5: Live Region Abuse and Timing Failures
Live regions are ARIA's mechanism for announcing dynamic content updates to screen reader users. They are powerful, but they require careful implementation. The failure modes I find in audits fall into three categories.
Using aria-live="assertive" for Everything
aria-live="assertive" interrupts whatever the screen reader is currently saying to make the announcement immediately. This is appropriate for genuine urgent alerts — a session expiration warning, a payment failure message. It is not appropriate for search result counts, success confirmation messages, or anything the user can afford to hear after finishing the current task.
When every notification on a site is assertive, users experience constant interruptions. I have tested sites where navigating to a new page with a keyboard causes three or four simultaneous assertive announcements. It is unusable.
<!-- Polite for non-urgent status messages -->
<div role="status" aria-live="polite" aria-atomic="true"></div>
<!-- Assertive ONLY for urgent errors or session warnings -->
<div role="alert" aria-live="assertive" aria-atomic="true"></div>
Injecting the Live Region After Content Is Already Inside It
Screen readers only announce live region updates when the content of an already-present live region changes. If you create the live region element and inject it into the DOM at the same time as the content, the announcement never fires — the region was not present when the content was added.
<!-- Wrong: region created and populated simultaneously -->
const div = document.createElement('div');
div.setAttribute('role', 'status');
div.setAttribute('aria-live', 'polite');
div.textContent = 'Item saved successfully.';
document.body.appendChild(div); // Too late — content was added at creation
<!-- Correct: region exists in HTML, content updated via JavaScript -->
// HTML: <div role="status" aria-live="polite" id="live-region"></div>
document.getElementById('live-region').textContent = 'Item saved successfully.';
The Pattern That Causes All of These Mistakes
Every one of these mistakes stems from the same underlying issue: ARIA is learned from documentation rather than from using the technology it is supposed to support.
Developers read the ARIA specification, understand what the attributes do in the abstract, and then implement them based on that abstract understanding. But the specification describes intent — it does not fully describe the practical behavior of NVDA, JAWS, VoiceOver, and TalkBack when encountering those attributes in real-world conditions.
The only way to know whether your ARIA implementation actually works is to test it with a screen reader. Not just run axe-core. Not just check the accessibility tree in DevTools. Actually navigate the page with a screen reader running and listen to what it announces.
When I audit a site, I spend more time with JAWS and NVDA running than I spend looking at the code. The code review matters — it tells me why something is broken. But the screen reader session tells me what the user actually experiences. Those are different things, and both of them matter.
Need a real accessibility audit?
Not a Lighthouse score — a thorough WCAG 2.2 AA audit with manual JAWS testing and code-level remediation guidance your developers can actually use.
Schedule a Free Consultation