← Back to Blog

Fix Common WCAG Violations: A Developer's Guide

8 min read

Automated accessibility scanners consistently flag the same set of violations across the web. The WebAIM Million project, which analyzes the top one million homepages annually, shows that a small number of issue types account for the vast majority of detectable WCAG failures. This guide covers the ten most common violations, explains why each one matters, and provides code examples to fix them.

1. color-contrast

WCAG criterion: 1.4.3 Contrast (Minimum) — Level AA

What it is: Text does not have sufficient contrast against its background. WCAG requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular).

Why it matters: Low-contrast text is difficult or impossible to read for users with low vision, color blindness, or anyone using a screen in bright sunlight. This is the single most common violation found on the web.

/* Before: contrast ratio 2.5:1 */
.subtitle {
  color: #aaaaaa;
  background: #ffffff;
}

/* After: contrast ratio 4.6:1 */
.subtitle {
  color: #767676;
  background: #ffffff;
}

2. image-alt

WCAG criterion: 1.1.1 Non-text Content — Level A

What it is: An <img> element is missing an alt attribute, or has an alt attribute that does not adequately describe the image's content or function.

Why it matters: Screen readers announce images using their alt text. Without it, users hear the file name or nothing at all, losing the information the image conveys.

<!-- Before: missing alt -->
<img src="chart-q4.png" />

<!-- After: descriptive alt text -->
<img src="chart-q4.png" alt="Bar chart showing Q4 revenue increased 23% compared to Q3" />

<!-- Decorative image: use empty alt -->
<img src="decorative-divider.svg" alt="" />

3. label

WCAG criterion: 1.3.1 Info and Relationships, 4.1.2 Name, Role, Value — Level A

What it is: A form input does not have an associated label. This happens when a <label> element is missing, its for attribute does not match the input's id, or aria-label / aria-labelledby is not provided.

Why it matters: Without a programmatic label, screen readers cannot tell users what a form field is for. Users must guess based on surrounding context, which is unreliable.

<!-- Before: no associated label -->
<input type="email" placeholder="Email" />

<!-- After: explicit label association -->
<label for="email-input">Email address</label>
<input type="email" id="email-input" placeholder="you@example.com" />

4. link-name

WCAG criterion: 4.1.2 Name, Role, Value — Level A

What it is: A link does not have discernible text. This commonly occurs with icon-only links (e.g., a social media icon wrapped in an anchor tag) or links that contain only an image without alt text.

Why it matters: Screen readers announce links by their accessible name. A link with no name is announced as simply “link” — useless for navigation.

<!-- Before: icon link with no accessible name -->
<a href="https://twitter.com/a11ymate">
  <svg><!-- twitter icon --></svg>
</a>

<!-- After: add aria-label -->
<a href="https://twitter.com/a11ymate" aria-label="A11yMate on Twitter">
  <svg aria-hidden="true"><!-- twitter icon --></svg>
</a>

5. button-name

WCAG criterion: 4.1.2 Name, Role, Value — Level A

What it is: A button element does not have an accessible name. This is the button equivalent of the link-name issue, and commonly appears on icon buttons (hamburger menus, close buttons, search icons).

Why it matters: A screen reader user encountering a button announced as just “button” has no way to know what it does without activating it, which could have unintended consequences.

<!-- Before: icon button with no name -->
<button>
  <svg><!-- close icon --></svg>
</button>

<!-- After: add aria-label -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- close icon --></svg>
</button>

<!-- Alternative: use visually hidden text -->
<button>
  <svg aria-hidden="true"><!-- close icon --></svg>
  <span class="sr-only">Close dialog</span>
</button>

6. html-has-lang

WCAG criterion: 3.1.1 Language of Page — Level A

What it is: The <html> element is missing a lang attribute, or the attribute has an invalid value.

Why it matters: Screen readers use the lang attribute to determine which speech synthesizer to use. Without it, the screen reader may pronounce text using the wrong language rules, making content incomprehensible. Search engines also use this attribute for language detection.

<!-- Before: missing lang attribute -->
<html>

<!-- After: specify the page language -->
<html lang="en">

<!-- For Korean content -->
<html lang="ko">

7. heading-order

WCAG criterion: 1.3.1 Info and Relationships — Level A

What it is: Heading levels are skipped (e.g., jumping from <h1> to <h4>). Headings should form a logical hierarchy that reflects the document structure.

Why it matters: Screen reader users frequently navigate by headings. A skipped level suggests missing content or a broken document structure, making it harder to build a mental model of the page. Many screen reader users report heading navigation as their primary method of finding content.

<!-- Before: skips from h1 to h4 -->
<h1>Product Documentation</h1>
<h4>Installation Guide</h4>
<h4>API Reference</h4>

<!-- After: logical heading hierarchy -->
<h1>Product Documentation</h1>
<h2>Installation Guide</h2>
<h2>API Reference</h2>
<h3>Authentication</h3>
<h3>Endpoints</h3>

8. region

WCAG criterion: 1.3.1 Info and Relationships — Level A (best practice)

What it is: Page content is not contained within landmark regions. Landmarks are defined by HTML5 sectioning elements (<header>, <nav>, <main>, <footer>) or ARIA landmark roles.

Why it matters: Landmarks allow screen reader users to jump directly to sections of the page (e.g., “skip to main content”). Without them, users must navigate through every element sequentially.

<!-- Before: no landmarks -->
<div class="header">...</div>
<div class="content">...</div>
<div class="footer">...</div>

<!-- After: semantic landmarks -->
<header>...</header>
<nav aria-label="Main navigation">...</nav>
<main>...</main>
<footer>...</footer>

9. bypass (skip navigation)

WCAG criterion: 2.4.1 Bypass Blocks — Level A

What it is: The page does not provide a mechanism to skip repeated blocks of content. Most commonly, this means there is no “skip to main content” link.

Why it matters: Keyboard and screen reader users encounter the same navigation menu on every page. Without a skip link, they must tab through every navigation item before reaching the main content — on every single page load.

<!-- Add a skip link as the first element in <body> -->
<body>
  <a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:text-sm">
    Skip to main content
  </a>
  <header><!-- navigation --></header>
  <main id="main-content">
    <!-- page content -->
  </main>
</body>

10. duplicate-id

WCAG criterion: 4.1.1 Parsing — Level A

What it is: Multiple elements on the page share the same id attribute value. The HTML specification requires that id values be unique within a document.

Why it matters: Duplicate IDs break the association between labels and form controls, cause aria-labelledby and aria-describedby references to resolve to the wrong element, and produce unpredictable behavior when JavaScript uses getElementById. In accessibility terms, this means screen readers may announce the wrong label for a form field or skip content entirely.

<!-- Before: duplicate id -->
<label for="name">First name</label>
<input id="name" type="text" />

<label for="name">Last name</label>
<input id="name" type="text" />

<!-- After: unique ids -->
<label for="first-name">First name</label>
<input id="first-name" type="text" />

<label for="last-name">Last name</label>
<input id="last-name" type="text" />

A Pattern You May Notice

Most of these violations share a common root cause: the visual appearance of the page works fine for sighted mouse users, but the underlying semantics are missing. A placeholder looks like a label but is not one. An SVG icon looks like a recognizable symbol but has no accessible name. A div looks like a button but is not announced as one.

The fix pattern is equally consistent: use semantic HTML elements, provide text alternatives for non-text content, and ensure programmatic relationships exist between related elements (labels to inputs, headings to sections, landmarks to content). When native HTML is not sufficient, ARIA attributes fill the gap — but only as a supplement, not a replacement.

Fix Faster with A11yMate

A11yMate has 37 built-in fix guides covering every violation listed here and more. When you scan a page, each detected issue links to a guide that explains the problem, shows before-and-after code, and references the relevant WCAG success criterion. Instead of searching documentation or Stack Overflow for each issue, you get the fix inline, in context, right where you are working.

The extension is free, runs entirely in your browser, and requires no account. Install it and start fixing accessibility issues today.

Try A11yMate free →