How to Fix 6 Common WCAG 2.2 AA ARIA Violations in Any Web App
accessibility wcag aria web-development frontend

How to Fix 6 Common WCAG 2.2 AA ARIA Violations in Any Web App

D. Rout

D. Rout

June 18, 2026 9 min read

On this page

Introduction

Accessibility audits have a way of producing the same six violations over and over, no matter what framework, design system, or team wrote the code. This post walks through exactly that: six recurring WCAG 2.2 AA ARIA violations, why each one happens, and the concrete fix for each — written for any web application, not tied to a specific framework.

If you've run an automated scanner like axe DevTools, ARC Toolkit, or Lighthouse against a real production app, these six issue types will look familiar:

  1. ARIA elements do not have accessible names
  2. ARIA elements missing child roles
  3. ARIA child roles missing their required parent element
  4. Image elements without alt attributes
  5. Elements using prohibited ARIA attributes
  6. Touch targets with insufficient size or spacing

We'll fix each one with plain HTML, CSS, and vanilla JavaScript, and build a small reference site as we go. Clone the companion GitHub repository to follow along with runnable before/after examples for every issue.

Prerequisites

  • Basic HTML, CSS, and JavaScript knowledge
  • A modern browser
  • A screen reader for verification — VoiceOver (Cmd + F5 on macOS) or NVDA (free, Windows)
  • Optionally, an automated accessibility scanner like axe DevTools to confirm fixes

No build tools or frameworks are required for any of the patterns below.

Step 1: Fix missing accessible names

An accessible name is what a screen reader speaks when it focuses an element. It can come from visible text, aria-label, aria-labelledby, or alt. Without one, assistive tech announces only the role — "button" — with zero context.

The most common trigger is an icon-only button:

<!-- ❌ Wrong: announces only "button" -->
<button>
  <svg><!-- close icon --></svg>
</button>
<!-- ✅ Fixed: announces "Close dialog, button" -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- close icon --></svg>
</button>

A second common trigger: relying on a placeholder instead of a real label. Placeholders disappear once text is entered and many screen readers skip them entirely.

<!-- ❌ Wrong -->
<input type="email" placeholder="Email address">
<!-- ✅ Fixed -->
<label for="email">Email address</label>
<input id="email" type="email" placeholder="you@example.com">

And a third: clickable <div> cards with no semantic element or accessible name at all.

<!-- ✅ Fixed: a real link with a full accessible name -->
<a href="/products/running-shoe" class="card">
  <img src="shoe.jpg" alt="">
  <span>Trail Runner X2 — $89</span>
</a>

Step 2: Fix ARIA elements missing child roles

Some ARIA roles require specific child roles — the spec calls this the "required owned elements" rule. role="list" expects listitem children; role="listbox" expects option children. Putting plain elements directly inside breaks that ownership chain.

<!-- ❌ Wrong: buttons with no listitem wrapper -->
<ul role="list">
  <button>Item one</button>
  <button>Item two</button>
</ul>
<!-- ✅ Fixed: correct list → listitem → button chain -->
<ul>
  <li><button>Item one</button></li>
  <li><button>Item two</button></li>
</ul>

If you need the button to fill a CSS grid cell directly without visual nesting, use display: contents on the <li> rather than removing it — that keeps the accessibility tree intact while letting the layout ignore the wrapper.

The same rule applies to custom listboxes:

<!-- ✅ Fixed: children carry the required option role -->
<div role="listbox" aria-label="Color" tabindex="0">
  <div role="option" aria-selected="true">Red</div>
  <div role="option" aria-selected="false">Blue</div>
</div>

Step 3: Fix ARIA child roles missing their parent element

This is the inverse of Step 2. Some roles only make sense inside a specific parent — the "required context role" rule. The single most common trigger for this violation:

<!-- ❌ Wrong: role="region" overwrites the implicit "list" role,
     orphaning the <li> children's "listitem" role -->
<ul role="region" aria-label="Recent posts">
  <li>Post one</li>
</ul>

ARIA roles replace implicit roles rather than adding to them. Once region is set, the <ul> no longer has a list role, so its children — which require a list ancestor to be valid listitems — become orphaned.

<!-- ✅ Fixed: region on a neutral wrapper; list role untouched -->
<div role="region" aria-label="Recent posts">
  <ul>
    <li>Post one</li>
  </ul>
</div>

The same pattern shows up with tabs:

<!-- ✅ Fixed: tab wrapped in its required tablist context -->
<div role="tablist" aria-label="Account sections">
  <button role="tab" aria-selected="true">Profile</button>
</div>

A useful mental model: a job title like "VP of Engineering" only means something inside a company org chart. Pull it out of context and list it on its own, and it's meaningless — there's no hierarchy giving it sense. ARIA child roles work the same way.

Step 4: Fix images missing alt attributes

The alt attribute tells screen readers what an image contains. Without it, some screen readers announce the raw file path — meaningless noise. There are two correct states, and the violation is having neither:

<!-- ❌ Wrong: announces the raw file path -->
<img src="assets/icons/user-profile-avatar-v2.png">
<!-- ✅ Fixed: descriptive alt for a meaningful image -->
<img src="assets/icons/user-profile-avatar-v2.png" alt="User profile photo">
<!-- ✅ Fixed: empty alt tells screen readers to skip a decorative image -->
<img src="divider-line.svg" alt="">

When an icon sits inside a button that already has its own accessible name, hide the icon entirely to avoid double-announcing:

<button aria-label="Delete item">
  <img src="trash.svg" alt="" aria-hidden="true">
</button>

CSS background-image is invisible to screen readers — there's no DOM node to attach an alt to. If a background image is meaningful, give its container an explicit role and label:

<div class="hero" role="img" aria-label="Skyline of Manhattan at sunset"></div>

Step 5: Fix elements using prohibited ARIA attributes

Every ARIA role has a defined set of attributes it's allowed to carry. Using a prohibited one isn't just ignored — it can produce incorrect or confusing announcements. The most common case: aria-label on a plain <div> or <span>, which has the implicit role="generic" and doesn't support naming attributes at all.

<!-- ❌ Wrong: aria-label has zero effect on role="generic" -->
<div aria-label="User card">...</div>
<!-- ✅ Fixed: use a role/element where aria-label is valid -->
<section aria-label="User card">...</section>

aria-checked is another frequent offender — it's only valid on checkbox, radio, switch, and menuitemcheckbox roles, not on a generic toggle button:

<!-- ❌ Wrong -->
<div role="button" aria-checked="true">Apply filter</div>
<!-- ✅ Fixed: use aria-pressed for toggle buttons instead -->
<button aria-pressed="true">Apply filter</button>

And avoid overriding a native element's role when the element you actually want already exists:

<!-- ❌ Wrong -->
<button role="link">Go to homepage</button>

<!-- ✅ Fixed -->
<a href="/">Go to homepage</a>

Step 6: Fix touch targets with insufficient size or spacing

WCAG 2.2 Success Criterion 2.5.8 requires interactive targets to be at least 24×24 CSS pixels, with enough spacing that adjacent targets don't overlap that minimum zone. Apple's Human Interface Guidelines and Google's Material Design both recommend going further, to 44×44px. The usual cause is an icon button styled to its visual size with no padding around the actual tap area.

/* ❌ Wrong: 16px visual icon, 16px tap area */
.icon-btn {
  width: 16px;
  height: 16px;
}
/* ✅ Fixed: padding expands the tap area while the icon stays visually small */
.icon-btn {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-btn svg {
  width: 20px;
  height: 20px;
}

Spacing matters as much as size — two 44px buttons with only a 2px gap functionally merge into one mis-tappable zone:

/* ❌ Wrong */
.action-row { display: flex; gap: 2px; }

/* ✅ Fixed: keeps each target's minimum zone from overlapping its neighbor */
.action-row { display: flex; gap: 8px; }

Reference: violation types and their fixes

Issue Root cause Fix pattern
Missing accessible names Icon-only buttons, placeholder-only inputs aria-label, real <label>, semantic elements
Missing child roles Custom widgets without required children Add the owned child role (listitem, option, row)
Missing parent element role="region" overwriting implicit roles Move the role to a neutral wrapper <div>
Missing alt attributes No alt at all on <img> Descriptive alt or empty alt="" for decorative images
Prohibited ARIA attributes Naming/state attributes on incompatible roles Match the attribute to a role that supports it
Insufficient touch targets Visual icon size used as the tap area Pad to 44×44px minimum, 8px+ gap between targets

What's next

  • Run an automated scanner (axe DevTools, ARC Toolkit, or Lighthouse) against your own app and map each finding to one of these six categories
  • Pair every fix with a manual screen reader pass — automated tools catch maybe a third of real accessibility issues
  • Build these patterns into a shared component library so the fix only has to happen once per component, not once per page
  • If your team is just getting started with the underlying ARIA role model, the companion theoretical post on all six ARIA role categories is a good next read

Further reading

Closing

None of these six fixes require a framework migration or a design system overhaul — they're targeted markup and CSS changes you can apply incrementally, component by component. The pattern that matters most is the audit loop: scan, match the finding to one of these six categories, apply the fix, and re-scan to confirm.

Every example above is live and runnable in the wcag-aria-fixes-demo repository, with the wrong and fixed versions side by side in each issue folder — clone it, open any index.html, and test with a screen reader to hear the difference yourself.

Share

Comments (0)

Join the conversation

Sign in to leave a comment on this post.

No comments yet. to be the first!