A Developer's Guide to WCAG Compliance

A Developer's Guide to WCAG Compliance

Accessibility

Accessibility should never be described as a feature - it is a quality. WCAG is the internationally recognised standard for web accessibility, and WCAG compliance is how developers turn that standard into working code. It covers everyone - including people with visual, auditory, motor, or cognitive conditions. For developers, WCAG is also good engineering. Accessible code is cleaner, more semantic, and better indexed by search engines.

This guide covers what WCAG compliance means in practice. It focuses on the criteria that matter most, with real code.

What Is WCAG and Why Does It Matter?

WCAG is published by the World Wide Web Consortium (W3C) under the Web Accessibility Initiative (WAI). The current stable version is WCAG 2.2, released in October 2023. WCAG 3.0 is in development but not yet a recommendation.

The guidelines are organised around four principles, summarised as POUR:

  • Perceivable - Users can perceive all content through at least one sense.
  • Operable - Users can operate the interface without a mouse or time constraint.
  • Understandable - Content and UI behave predictably and clearly.
  • Robust - Content works across browsers, assistive technologies, and devices.

Each guideline has three conformance levels:

  • Level A - minimum requirements. Failing these creates serious barriers.
  • Level AA - the standard required by most legislation (UK Equality Act, EU EN 301 549, US Section 508).
  • Level AAA - enhanced accessibility, not always achievable for all content.

Most projects should target WCAG 2.2 Level AA. Understanding WCAG compliance requirements early in your design and development is the fastest route to solid web accessibility.

1. Use Semantic HTML First

The single highest-return investment in web accessibility is using the right HTML element for the right job. Semantic elements carry built-in roles, names, and states that assistive technologies understand natively. Getting this right cuts the WCAG compliance load across every other area.

What to do

Use native interactive elements wherever one exists. A <button> is focusable, activates on Enter and Space, exposes the correct ARIA role, and fires click events from the keyboard. A styled <div> does none of that without extra work.

<!-- Avoid -->
<div class="button" onclick="submitForm()">Submit</div>
<!-- Prefer -->
<button type="submit">Submit</button>

Use real headings for document structure, not styled paragraphs. Screen reader users rely on the heading outline to scan and navigate a page, so do not skip levels and do not pick a heading purely for its visual size.

<!-- Bad: heading hierarchy breaks screen reader navigation -->
<p class="big-text">About Us</p>
<p class="medium-text">Our Team</p>

<!-- Good: headings create a navigable document outline -->
<h2>About Us</h2>
<h3>Our Team</h3>

Use landmark elements to give screen reader users a map of the page:

<header>...</header>
<nav aria-label="Main navigation">...</nav>
<main>
  <article>...</article>
  <aside>...</aside>
</main>
<footer>...</footer>

Each landmark exposes a region that assistive technologies can jump between. Use only one <main> per page, and label multiple <nav> or <aside> elements with aria-label so they can be told apart.

Use lists for groups of related items, including navigation menus. Wrapping links in a <ul> tells assistive technology how many items there are and where the group ends.

<ul>
  <li><a href="/about">About</a></li>
  <li><a href="/work">Work</a></li>
  <li><a href="/contact">Contact</a></li>
</ul>

Use real tables for tabular data, with <th> for column or row headers and a scope attribute so the relationship between cells and headers is unambiguous. Never use a table for layout - this isn't the old days of web development.

<table>
  <caption>Monthly traffic</caption>
  <thead>
    <tr><th scope="col">Month</th><th scope="col">Visits</th></tr>
  </thead>
  <tbody>
    <tr><th scope="row">January</th><td>12,430</td></tr>
  </tbody>
</table>

Relevant WCAG criteria: 1.3.1 Info and Relationships (A), 4.1.2 Name, Role, Value (A)

2. Keyboard Navigation

Every interactive element must be reachable and usable with a keyboard alone. This is a core web accessibility requirement. It matters for motor-impaired users, power users, and anyone who prefers not to use a mouse.

Focus order and visibility

/* Never do this without a replacement */
:focus { outline: none; }

/* Do this instead — a visible, high-contrast focus indicator */
:focus-visible {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
  border-radius: 2px;
}

WCAG 2.2 introduced 2.4.11 Focus Appearance (AA). This requires focus indicators to meet a minimum area and contrast ratio. The safest approach is a solid 2–3px outline in a colour with at least 3:1 contrast against the adjacent colour.

Tab order

The default tab order follows DOM order. Avoid using tabindex values greater than 0, as they create a separate tab sequence that confuses users:

<!-- Avoid: positive tabindex breaks the natural sequence -->
<a href="/home" tabindex="3">Home</a>
<a href="/about" tabindex="1">About</a>
<a href="/contact" tabindex="2">Contact</a>

Use tabindex="0" to add a non-interactive element to the natural tab sequence, and tabindex="-1" for elements that should only receive focus programmatically:

<!-- Adds a custom element to the natural tab order -->
<div tabindex="0" role="button">Custom button</div>
<!-- Focusable only via JavaScript (e.g. for managing focus) -->
<div tabindex="-1">...</div>

Keyboard events for custom components

If you build a custom interactive component, you must handle keyboard events alongside mouse events:

function handleActivation(event) {
  if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') {
    event.preventDefault();
    doAction();
  }
}

element.addEventListener('click', handleActivation);
element.addEventListener('keydown', handleActivation);

Relevant WCAG criteria: 2.1.1 Keyboard (A), 2.4.3 Focus Order (A), 2.4.7 Focus Visible (AA), 2.4.11 Focus Appearance (AA)

3. Colour Contrast

Low contrast is one of the most common WCAG compliance failures and one of the easiest to fix. It is also a visible, testable aspect of web accessibility that can be checked before a line of CSS ships. It also affects people with low vision, colour blindness, and anyone reading in bright light.

The numbers to know

  • Normal text (under 18pt / 14pt bold) - 4.5:1 minimum (AA), 7:1 enhanced (AAA).
  • Large text (18pt / 14pt bold and above) - 3:1 minimum (AA), 4.5:1 enhanced (AAA).
  • UI components and graphical objects - 3:1 minimum (AA); no AAA requirement.
/* Fails AA at approximately 2.5:1 */
.bad {
  color: #999999;
  background-color: #ffffff;
}

/* Passes AA at approximately 7:1 */
.good {
  color: #595959;
  background-color: #ffffff;
}

Tools for checking contrast

Relevant WCAG criteria: 1.4.3 Contrast (Minimum) (AA), 1.4.6 Contrast (Enhanced) (AAA), 1.4.11 Non-text Contrast (AA)


4. Images and Alt Text

All meaningful images need a text alternative that conveys the same information. Decorative images must be hidden from assistive technologies to avoid noise.

<!-- Meaningful image: describe what it communicates, not what it looks like -->
<img src="chart.png" alt="Bar chart showing a 42% increase in mobile traffic between January and March 2026" />

<!-- Decorative image: empty alt hides it from screen readers -->
<img src="divider.png" alt="" role="presentation" />

<!-- Icon with adjacent text: hide the icon to avoid duplication -->
<button>
  <svg aria-hidden="true" focusable="false">...</svg>
  Download report
</button>

<!-- Icon-only button: the label must come from aria-label or aria-labelledby -->
<button aria-label="Close dialog">
  <svg aria-hidden="true" focusable="false">...</svg>
</button>

Don't write "image of" or "photo of" in alt text. Screen readers already announce it as an image.

Relevant WCAG criteria: 1.1.1 Non-text Content (A)

5. Forms and Error Handling

Forms are high-stakes for accessibility. Every input needs a label, errors must be described in text (not colour alone), and error messages must be specific.

Labels

<!-- Bad: placeholder disappears on focus and has low contrast -->
<input type="email" placeholder="Email address" />

<!-- Good: always-visible label associated via for/id -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" autocomplete="email" />

Error messages

<label for="email">Email address</label>
<input
  type="email"
  id="email"
  name="email"
  aria-describedby="email-error"
  aria-invalid="true"
/>
<p id="email-error" role="alert">
  Enter a valid email address, for example name@example.com
</p>

Key points:

  • Use aria-describedby to associate the error message with the field.
  • Set aria-invalid="true" when a field has an error.
  • Use role="alert" on dynamically injected error messages so screen readers announce them immediately.
  • Never rely on colour alone to indicate an error - add an icon, text, or both.

Required fields

<!-- HTML5 required attribute is announced by screen readers -->
<input type="email" id="email" required aria-required="true" />

Relevant WCAG criteria:

  • 1.3.5 Identify Input Purpose (AA)
  • 1.4.1 Use of Colour (A)
  • 3.3.1 Error Identification (A)
  • 3.3.2 Labels or Instructions (A)
  • 3.3.3 Error Suggestion (AA)

6. ARIA: Use It Sparingly

ARIA (Accessible Rich Internet Applications) lets you add semantic meaning where HTML alone falls short. But the first rule of ARIA is: do not use ARIA if you can use a native HTML element instead.

Misuse of ARIA is a leading cause of web accessibility failures that automated WCAG compliance checks can miss entirely. In fact, misused ARIA creates more barriers than it removes. A <div role="button"> without keyboard handling is worse than a - it announces as a button but does not work like one.

When ARIA is appropriate

Use ARIA for patterns that have no native HTML equivalent:

<!-- Disclosure/accordion -->
<button aria-expanded="false" aria-controls="section1">What is WCAG?</button>
<div id="section1" hidden>...</div>

<!-- Tab panel -->
<div role="tablist" aria-label="Account settings">
  <button role="tab" aria-selected="true" aria-controls="panel-profile" id="tab-profile">Profile</button>
  <button role="tab" aria-selected="false" aria-controls="panel-security" id="tab-security">Security</button>
</div>
<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">...</div>

<!-- Live region for dynamic content -->
<div aria-live="polite" aria-atomic="true" id="status"></div>

Refer to the ARIA Authoring Practices Guide for keyboard interaction patterns for every common widget.

Relevant WCAG criteria: 4.1.2 Name, Role, Value (A), 4.1.3 Status Messages (AA)

7. Responsive Design and Text Sizing

Users should be able to resize text and zoom the page without losing content or functionality.

/* Use relative units so text scales with user preferences */
body {
  font-size: 1rem; /* respects browser default, usually 16px */
  line-height: 1.5;
}

h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }

/* Ensure layout reflows at 400% zoom (320px viewport equivalent) */
@media (max-width: 320px) {
  .sidebar {
    display: none;
  }
}

Never disable zoom in the viewport meta tag:

<!-- Bad: prevents users from zooming -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />

<!-- Good -->
<meta name="viewport" content="width=device-width, initial-scale=1" />

Relevant WCAG criteria: 1.4.4 Resize Text (AA), 1.4.10 Reflow (AA)

8. Motion and Animation

Some users experience nausea or seizures from motion. Respect the prefers-reduced-motion media query:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Relevant WCAG criteria: 2.3.3 Animation from Interactions (AAA), 2.3.1 Three Flashes or Below Threshold (A)

9. Testing Your Implementation

Automated tools are a good first pass. They catch roughly 30–40% of accessibility issues - the rest need human judgement. The two tools worth having in every developer's browser are axe DevTools and Lighthouse. The axe DevTools extension (Chrome and Firefox) runs against the rendered DOM. It flags WCAG violations with the specific criteria, affected elements, and suggested fixes. It uses the axe-core engine - the same library behind most CI accessibility pipelines. That means browser findings and test suite results stay consistent. Run it on every meaningful page state - not just on load. Test after modals open, after form errors appear, and after dynamic content updates.

Lighthouse is built into Chrome DevTools under the Audits or Lighthouse panel, so there is nothing to install. Its accessibility score is a combined total of axe-core checks plus a handful of extra rules. A score of 100 doesn't mean the page is fully accessible - it just passed the automated checks. Anything below 90 usually signals something genuinely broken. Lighthouse is easy to add to a CI pipeline via the lighthouse-ci package. Use it to set a score threshold and fail pull requests if the score drops.

Diagnostic CSS

A small stylesheet applied during development can surface common issues visually without any tooling. Add these rules to a dev-only stylesheet or toggle them via a bookmarklet:

/* Flag images missing alt text */
img:not([alt]) {
  outline: 4px solid red;
}

/* Flag inputs not associated with a label */
input:not([aria-label]):not([aria-labelledby]):not([id]),
select:not([aria-label]):not([aria-labelledby]):not([id]),
textarea:not([aria-label]):not([aria-labelledby]):not([id]) {
  outline: 4px solid orange;
}

/* Flag links with no href or empty text */
a:not([href]),
a[href=""] {
  outline: 4px solid red;
}

/* Flag positive tabindex values that break natural tab order */
[tabindex]:not([tabindex="-1"]):not([tabindex="0"]) {
  outline: 4px solid purple;
}

/* Make focus rings impossible to miss during testing */
*:focus {
  outline: 4px dashed hotpink !important;
  outline-offset: 4px !important;
}

These rules won't catch everything. But they make the most common structural problems visible as you browse normally.

Manual testing checklist

  • Navigate the entire page using only the keyboard (Tab, Shift+Tab, Enter, Space, arrow keys)
  • Check focus is always visible and logical
  • Test with a screen reader: NVDA + Firefox on Windows, VoiceOver + Safari on macOS/iOS
  • Zoom to 200% and 400% - confirm no content is lost
  • Disable CSS - check that content still makes sense in source order
  • Test with Windows High Contrast mode enabled
  • Check all form validation states (error, success, required)

10. Accessibility in Your Development Workflow

In practice, retrofitting accessibility is expensive. Fixing WCAG compliance issues after launch means unpicking decisions baked into the design, component library, and content. Those fixes typically land under time pressure, with less time to do them well. Building it in from the start costs relatively little. A linter rule here, a CI audit there, and a shared definition of "done" - that’s all it takes.

So, the most effective change is treating accessibility like test coverage - an engineering quality bar, not a post-launch checklist. Raise issues where they're cheapest to fix - in design review, or in a pull request before shipping. Not in a bolt-on audit six months later.

In your tooling and CI pipeline:

  • Add eslint-plugin-jsx-a11y (React) or the equivalent for your framework. This surfaces violations in your editor before code is even committed.
  • Include axe-core checks in your component tests or end-to-end suite so regressions are caught automatically as a build requirement.
  • Review designs before implementation for colour contrast, touch target sizes, and focus order.
  • Catching issues in Figma takes minutes; catching them in production takes a sprint.
  • Agree on a component-level accessibility contract for your design system.
  • Build a Button or Modal correctly once. Every instance then inherits the behaviour.

In your ongoing quality practice:

  • Schedule periodic screen reader walkthroughs of key user journeys. Automated checks can still miss problems.
  • Reading order, headings, and live region announcements all need human review.
  • Include a short accessibility primer when onboarding new team members. Most violations come from unfamiliarity, not indifference.

Summary

WCAG compliance is achievable without specialist knowledge. Most of it is good HTML, thoughtful CSS, and a few well-placed ARIA attributes. The biggest wins come from:

  1. Using semantic HTML elements instead of div soup
  2. Never suppress focus indicators
  3. Meeting colour contrast ratios
  4. Writing meaningful alt text
  5. Associating form labels and error messages correctly

Together, these five will resolve the majority of real-world accessibility barriers and push most pages to WCAG 2.2 AA conformance. Web accessibility is not a niche concern - it is a measurable quality standard. WCAG compliance gives developers the concrete criteria to meet it.

Comments (0)

No comments yet — be the first to share your thoughts.

Leave a comment

0/500 characters

Your comment will be checked before it appears publicly.