ARIA Patterns That Break Screen Readers (And What to Use Instead)
Common ARIA anti-patterns found in 10,000 scans. Role conflicts, missing states, and the tabindex trap.
ARIA: The Most Misused Tool in Web Development
ARIA (Accessible Rich Internet Applications) was designed to bridge gaps where native HTML semantics fall short. But in practice, it's the #1 source of accessibility violations we find. In 10,000 scans, 83% of sites had at least one ARIA error — and most had more than five.
The first rule of ARIA is: don't use ARIA. If a native HTML element provides the semantics you need, use that instead. A <button> is always better than <div role='button'>. A <nav> is always better than <div role='navigation'>.
The First Rule of ARIA
If you can use a native HTML element or attribute with the semantics and behavior you require already built in, do so. Don't use ARIA.
Pattern #1: Role Conflicts
The most common mistake is applying ARIA roles that conflict with the element's native semantics. When you add role='button' to an <a> tag, you're telling the screen reader 'this is a button' — but the browser still treats it as a link. The result: confused behavior.
Screen readers may not announce the correct interaction model. NVDA says 'button' but Enter behaves like a link navigation. VoiceOver may ignore the role entirely on certain elements.
- Never put role='button' on an <a> tag — use <button> with an onClick
- Never put interactive roles on heading elements — they lose heading semantics
- Role on <input> is almost always redundant — use the correct type attribute
- Don't use role='presentation' on elements that have required children (like <table>)
// ❌ Bad: Role conflicts with native semantics
<a href="/page" role="button">Click me</a>
<h2 role="tab">Section Title</h2>
<input type="text" role="search" /> // role on input is mostly redundant
// ✅ Good: Use native elements
<button onClick={() => navigate('/page')}>Click me</button>
<button role="tab" aria-selected="true">Section Title</button>
<input type="search" /> // type provides semanticsPattern #2: Missing Required States
When you use ARIA roles that require specific states, omitting those states is worse than not using ARIA at all. A role='checkbox' without aria-checked is a checkbox in limbo — the screen reader announces 'checkbox' but can't tell the user if it's checked.
Required states are non-negotiable. The WAI-ARIA specification lists exactly which attributes are required for each role. RegLayer's scanner checks all 78 required state combinations.
// ❌ Bad: Missing required states <div role="checkbox">Accept terms</div> <div role="tab">Tab 1</div> <div role="combobox">Select...</div> // ✅ Good: All required states present <div role="checkbox" aria-checked="false" tabindex="0">Accept terms</div> <div role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</div> <div role="combobox" aria-expanded="false" aria-controls="listbox-1">Select...</div>
Pattern #3: The tabindex Trap
tabindex='0' makes an element focusable in DOM order — fine. tabindex='-1' removes from tab order but allows programmatic focus — also fine. tabindex with any positive value? That's where chaos begins.
Positive tabindex values create a custom tab order that overrides DOM order. This makes navigation unpredictable, breaks skip links, and creates maintenance nightmares. Every time someone adds an element, the entire tab order shifts. We see this in 34% of scanned sites.
- Never use tabindex > 0 — it creates unmaintainable tab orders
- Use tabindex='0' to make custom widgets focusable in natural DOM order
- Use tabindex='-1' for elements that should only receive programmatic focus
- If you need to reorder focus, reorder the DOM instead
// ❌ Bad: Positive tabindex creates unpredictable order <input tabindex="3" /> <button tabindex="1">Submit</button> <a tabindex="2" href="/help">Help</a> // ✅ Good: Use DOM order + tabindex="0" for custom elements <a href="/help">Help</a> <input /> <button>Submit</button> <div role="button" tabindex="0">Custom button</div>
Pattern #4: aria-live Abuse
aria-live regions are supposed to announce dynamic content changes to screen readers. But when overused, they create a cacophony of announcements that overwhelm users.
The most common mistake: putting aria-live='assertive' on elements that update frequently (like timers, stock tickers, or typing indicators). Each update interrupts whatever the screen reader is currently saying. Use 'polite' for most updates, and 'assertive' only for critical alerts.
// ❌ Bad: Assertive on frequently-updating content
<div aria-live="assertive">{typingStatus}</div>
<div aria-live="assertive">Score: {score}</div>
// ✅ Good: Polite for non-critical, assertive only for errors
<div aria-live="polite">{searchResultCount} results</div>
<div role="alert">Payment failed. Please try again.</div>Find ARIA issues in your site
RegLayer's scanner detects 47 distinct ARIA violation patterns. Run a free scan.
Run Free Scan