Blog

How to Fix Accessibility Errors: Common WCAG Violations Solved

TestParty
TestParty
May 8, 2025

You've run an accessibility audit—maybe using axe, WAVE, or Lighthouse—and now you're staring at a list of errors. Some make sense immediately; others seem cryptic. Where do you start? What do these violations actually mean? How do you fix them without breaking something else?

This guide takes the most common accessibility errors and walks through practical fixes. No theory, no WCAG specification deep-dives—just solutions you can implement today.


Missing Alternative Text

This error appears more often than any other. When an image lacks an alt attribute, screen readers either skip it entirely or announce the filename, which rarely helps anyone understand the content.

The Fix

Every <img> element needs an alt attribute. What you put in that attribute depends on the image's purpose.

Informative images that convey content get descriptive text:

<!-- Before -->
<img src="quarterly-results-chart.png">

<!-- After -->
<img src="quarterly-results-chart.png"
     alt="Bar chart showing Q4 revenue increased 23% compared to Q3">

The description should convey the information the image communicates. For a chart, that's usually the key takeaway, not a literal description like "a bar chart with four bars."

Decorative images that don't add information get empty alt text:

<!-- Before -->
<img src="decorative-swirl.png">

<!-- After -->
<img src="decorative-swirl.png" alt="">

Empty alt (not missing alt—there's a difference) tells screen readers to skip the image entirely. This is correct for decorative elements that would just add noise.

Functional images inside links or buttons get text describing the function:

<!-- Before -->
<a href="/search">
  <img src="search-icon.svg">
</a>

<!-- After -->
<a href="/search">
  <img src="search-icon.svg" alt="Search">
</a>

Common Mistakes

Writing "image of" or "photo of" wastes words—screen readers already announce it's an image. Just describe the content directly.

Using the filename as alt text almost never helps. "IMG_2847.jpg" tells users nothing useful.


When a link or button contains no text, screen readers announce "link" or "button" without any indication of purpose. Users have no idea where the link goes or what the button does.

The Fix for Links

Links need either visible text content or an accessible name via ARIA:

<!-- Before: Empty link, icon only -->
<a href="/cart" class="cart-icon">
  <svg><!-- shopping cart icon --></svg>
</a>

<!-- After: Option 1 - Visually hidden text -->
<a href="/cart" class="cart-icon">
  <span class="visually-hidden">Shopping cart</span>
  <svg aria-hidden="true"><!-- shopping cart icon --></svg>
</a>

<!-- After: Option 2 - aria-label -->
<a href="/cart" class="cart-icon" aria-label="Shopping cart">
  <svg aria-hidden="true"><!-- shopping cart icon --></svg>
</a>

The visually hidden text approach keeps the accessible name in the HTML where it's visible to developers maintaining the code. The aria-label approach is more concise but can be easier to miss during updates.

The CSS for visually hidden text:

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

The Fix for Buttons

Same principle—buttons need accessible names:

<!-- Before: Empty button -->
<button onclick="closeModal()">
  <svg><!-- X icon --></svg>
</button>

<!-- After -->
<button onclick="closeModal()" aria-label="Close dialog">
  <svg aria-hidden="true"><!-- X icon --></svg>
</button>

Always mark decorative icons with aria-hidden="true" to prevent screen readers from attempting to announce them.


Missing Form Labels

Form fields without labels are nearly unusable for screen reader users. They hear "edit text" with no context about what information the field expects.

The Fix

Associate every input with a label using matching for and id attributes:

<!-- Before: No label -->
<input type="text" name="email" placeholder="Enter your email">

<!-- After: Proper label -->
<label for="email">Email Address</label>
<input type="email" id="email" name="email">

If your design doesn't include visible labels (not recommended, but sometimes unavoidable), use aria-label:

<!-- Hidden label via ARIA -->
<input type="search" aria-label="Search products" placeholder="Search...">

For fields where the label text exists elsewhere on the page, use aria-labelledby:

<!-- Table cell input labeled by column header -->
<input type="text" aria-labelledby="qty-header">

Placeholder text is never an acceptable substitute for labels. It disappears when users type, has contrast issues, and isn't consistently supported by assistive technologies.


Insufficient Color Contrast

Low contrast text might look sleek to designers, but it's difficult to read for users with low vision, color blindness, or anyone viewing in bright sunlight or on a low-quality display.

WCAG Requirements

Normal text needs a 4.5:1 contrast ratio against its background. Large text (18pt/24px regular or 14pt/18.5px bold) needs 3:1. Non-text elements like icons and borders need 3:1.

The Fix

Adjust either the text color or background until you meet the threshold:

/* Before: Gray on white - 2.5:1 ratio (fails) */
.light-text {
  color: #999999;
  background: #ffffff;
}

/* After: Darker gray - 4.6:1 ratio (passes) */
.light-text {
  color: #757575;
  background: #ffffff;
}

Use contrast checking tools during development. Browser DevTools show contrast ratios when you inspect colors. WebAIM's contrast checker lets you test arbitrary combinations.

If your brand colors don't meet contrast requirements, you have options: darken them slightly for text use, reserve them for large decorative elements, or use them only for non-essential visual treatments.


Missing Document Language

When the HTML document doesn't specify a language, screen readers may pronounce words using incorrect pronunciation rules. French text read with English pronunciation becomes incomprehensible.

The Fix

Add the lang attribute to the html element:

<!-- Before -->
<html>

<!-- After -->
<html lang="en">

Use the appropriate ISO language code: en for English, es for Spanish, fr for French, de for German, and so on.

For content in a different language than the page default, add lang to that specific element:

<html lang="en">
<body>
  <p>The French phrase <span lang="fr">c'est la vie</span> means "that's life."</p>
</body>
</html>

Heading Hierarchy Issues

Screen reader users often navigate by headings—it's one of the most efficient ways to understand page structure. Skipped heading levels (like going from H1 to H3) or multiple H1 elements confuse this navigation.

The Fix

Use headings in sequential order without skipping levels:

<!-- Before: Skipped H2, multiple H1s -->
<h1>Our Company</h1>
<h1>Welcome</h1>
<h3>Services</h3>

<!-- After: Proper hierarchy -->
<h1>Our Company</h1>
<h2>Services</h2>
<h3>Web Development</h3>
<h3>Design</h3>
<h2>About Us</h2>

Each page should have exactly one H1, typically matching or relating to the page title. Subsequent sections use H2, subsections use H3, and so on.

If you've been using heading tags for visual styling rather than structure, switch to CSS:

<!-- Before: H3 used for styling -->
<h3 class="sidebar-title">Recent Posts</h3>

<!-- After: Appropriate heading with CSS styling -->
<h2 class="sidebar-title">Recent Posts</h2>
.sidebar-title {
  font-size: 1rem;  /* Style it however needed */
  font-weight: 600;
}

Users who navigate by keyboard must tab through every navigation link to reach main content. On sites with extensive navigation, this means dozens of keystrokes on every page.

The Fix

Add a skip link at the very beginning of the page body:

<body>
  <a href="#main-content" class="skip-link">Skip to main content</a>

  <header><!-- Navigation --></header>

  <main id="main-content"><!-- Content --></main>
</body>

Style it to appear only when focused:

.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: #000;
  color: #fff;
  padding: 8px 16px;
  z-index: 100;
}

.skip-link:focus {
  top: 0;
}

This hides the link from sighted mouse users while making it the first thing keyboard users encounter.


Focus Not Visible

When focus indicators are removed or too subtle, keyboard users can't see where they are on the page. They're navigating blind.

The Fix

Never remove focus outlines without providing an alternative:

/* WRONG: Removes all focus indication */
*:focus {
  outline: none;
}

/* RIGHT: Custom focus style */
*:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

The :focus-visible pseudo-class applies focus styles only for keyboard navigation, not mouse clicks—the best of both worlds.

Make sure your focus indicator is visible against all backgrounds:

/* Default focus */
:focus-visible {
  outline: 2px solid #0066cc;
  outline-offset: 2px;
}

/* Focus on dark backgrounds */
.dark-section :focus-visible {
  outline-color: #ffffff;
}

Keyboard Traps

When users can navigate into a component but can't navigate out using keyboard, they're trapped. This commonly happens with modals, embedded content, or poorly implemented widgets.

The Fix

Ensure every interactive element can be reached and escaped via keyboard:

// Modal focus management
function openModal() {
  // Store current focus
  previousFocus = document.activeElement;

  // Show modal and move focus inside
  modal.hidden = false;
  modal.querySelector('button, [href], input').focus();
}

function closeModal() {
  // Hide modal
  modal.hidden = true;

  // Return focus to trigger
  previousFocus.focus();
}

// Close on Escape key
modal.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    closeModal();
  }
});

For embedded content like iframes that trap focus, ensure there's always a way out—typically via an Escape key handler.


Invalid ARIA Usage

ARIA can enhance accessibility, but incorrect ARIA often makes things worse. Common mistakes include using ARIA roles that conflict with native semantics, referencing IDs that don't exist, or using ARIA attributes on elements that don't support them.

The Fix

Wrong role on native element:

<!-- Before: Conflicting semantics -->
<button role="link">Click here</button>

<!-- After: Use the right element -->
<a href="/somewhere">Click here</a>

Missing referenced element:

<!-- Before: aria-describedby references non-existent ID -->
<input aria-describedby="name-hint">

<!-- After: Ensure the referenced element exists -->
<input aria-describedby="name-hint">
<p id="name-hint">Enter your full legal name</p>

Invalid ARIA on non-interactive element:

<!-- Before: aria-pressed on non-interactive element -->
<div aria-pressed="true">Toggle</div>

<!-- After: Use appropriate element with ARIA -->
<button aria-pressed="true">Toggle</button>

The first rule of ARIA: if you can use native HTML to achieve the behavior, do that instead. A <button> is always better than a <div role="button">.


Taking Action Systematically

When faced with a long error list, prioritize by impact:

Fix first: Errors that completely block functionality—keyboard traps, missing labels, empty links Fix second: Errors affecting comprehension—missing alt text, heading issues, low contrast Fix third: Technical violations that may not immediately impact users—ARIA issues, language declaration

Work through your site systematically, testing as you go. Automated tools catch about 30-40% of issues, so combine automated scanning with manual keyboard and screen reader testing.

TestParty provides continuous monitoring that catches these issues as they're introduced, preventing accessibility debt from accumulating.

Schedule a TestParty demo and get a 14-day compliance implementation plan.


Stay informed

Accessibility insights delivered
straight to your inbox.

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo