# Why I Built a WCAG 2.1 / 2.2 AA + 3.0 Draft Accessibility Audit Tool (and How to Fix Every Issue It Finds)

A free in-browser accessibility audit that mirrors WAVE WebAIM — covers WCAG 2.1 AA baseline, the six new 2.2 AA outcomes, the 3.0 draft readability outcome, exports CSV / HTML / PDF, and hands you an AI fix prompt. Here is why it exists, what it finds, and how to fix every issue yourself.

Author: J.A. Watte
Published: April 19, 2026
Source: https://jwatte.com/blog/blog-wcag-accessibility-audit/

---

I added a new tool to the hub: the [WCAG Accessibility Audit](/tools/wcag-accessibility-audit/). It pastes a URL, pulls the static HTML through our Netlify proxy, and runs 70+ programmatic accessibility checks in your browser. It exports the result as CSV, standalone HTML, or print-ready PDF, and it hands you an AI fix prompt ready to paste into Claude or ChatGPT. This post explains why I built it, what it catches, and how to remediate every failure category yourself.

## Why this tool exists

The single most useful free accessibility tool on the web is [WAVE by WebAIM](https://wave.webaim.org/report). It is maintained by the same team at Utah State University that runs the WebAIM Million survey, and its engine is effectively the industry reference. I use it constantly.

But WAVE is a browser extension + a hosted web report. I wanted three things it does not give me directly:

1. **Batch-friendly output.** If I need to audit fifty pages before a launch, I do not want to paste URLs into WAVE one at a time and screenshot the result. I want a CSV I can pivot against my sitemap.
2. **WCAG 2.2 AA coverage, explicit.** WAVE reports against WCAG 2.1. WCAG 2.2 was published in October 2023 and added six new AA-level success criteria, target size, dragging movements, focus not obscured, consistent help, redundant entry, and accessible authentication. Procurement teams are already asking for 2.2 conformance.
3. **An AI fix prompt.** The jump from "here is the list of failures" to "here is a patch I can ship" is where most accessibility audits stall. I wanted a one-click copy that produces a remediation-focused prompt for Claude / ChatGPT / Cursor, scoped tightly so it fixes the markup without redesigning my page.

The new tool does all three. And it is explicit about what it cannot do: **static HTML does not let you verify computed color contrast, focus order, keyboard traps, screen-reader announcements, motion-reduction preferences, or anything that requires rendering the page**. For any page that matters, download it, test it in a real browser, run it through NVDA or VoiceOver, and cross-validate the automated findings against [wave.webaim.org/report](https://wave.webaim.org/report). That is the ground truth.

## What the tool actually checks

The full list is in the tool itself, grouped by WCAG principle. Highlights:

### Perceivable (WCAG 1.x)
- **1.1.1 Non-text content.** Images missing `alt`, decorative-sized images with empty alt, filename-as-alt (`alt="image1.jpg"`), alt text longer than 250 characters, image-only links with no accessible name, image-map areas missing alt.
- **1.2.2 Captions (prerecorded).** `<video>` elements with no `<track kind="captions">`. Flags autoplay media because 1.4.2 (audio control) is adjacent.
- **1.3.1 Info and relationships.** Data tables missing `<th>` / `<thead>`, lists containing non-`<li>` children, missing `<main>` landmark, multiple `<main>` elements, missing `<nav>` / `<header>` / `<footer>` landmarks, radio groups not wrapped in `<fieldset><legend>`.
- **1.3.5 Identify input purpose.** Personal-data inputs (email, phone, name, address) missing the `autocomplete` attribute.
- **1.4.4 Resize text / 1.4.10 Reflow.** Viewport meta with `user-scalable=no` or `maximum-scale=1`. Elements with fixed pixel widths >400px.
- **1.4.13 Content on hover or focus.** Elements relying on `title` attribute alone for their accessible name.

### Operable (WCAG 2.x)
- **2.1.1 Keyboard.** `onclick` on non-interactive elements with no `tabindex` or `role`.
- **2.2.1 Timing adjustable.** `<meta http-equiv="refresh">`.
- **2.2.2 Pause, stop, hide.** `autoplay` on `<video>` / `<audio>`.
- **2.4.1 Bypass blocks.** No skip link.
- **2.4.2 Page titled.** Missing or too-short `<title>`.
- **2.4.4 Link purpose.** Generic link text ("click here", "read more", "more"), empty links with no `aria-label`.
- **2.4.6 Headings and labels.** Zero or multiple `<h1>`, empty headings, skipped heading levels, repeated heading text.
- **2.4.7 Focus visible.** `outline:none` / `outline:0` in CSS without a documented `:focus-visible` replacement.
- **2.4.11 Focus not obscured (NEW in 2.2 AA).** Sticky/fixed elements detected, flagged as a manual-verify reminder.
- **2.5.3 Label in name.** `aria-label` that does not contain the visible text.
- **2.5.7 Dragging movements (NEW in 2.2 AA).** `draggable="true"`, needs a non-drag alternative.
- **2.5.8 Target size (NEW in 2.2 AA).** Interactive targets declaring width/height under 24px.

### Understandable (WCAG 3.x)
- **3.1.1 Language of page.** Missing `lang` on `<html>`.
- **3.2.2 On input.** `<select onchange="...">` that navigates or submits.
- **3.2.6 Consistent help (NEW in 2.2 AA).** Help / contact / FAQ link present.
- **3.3.2 Labels or instructions.** Form controls missing `<label for>`, `aria-label`, or `aria-labelledby`.
- **3.3.7 Redundant entry (NEW in 2.2 AA).** Forms asking for the same information in multiple fields.
- **3.3.8 / 3.3.9 Accessible authentication (NEW in 2.2 AA).** Password forms alongside CAPTCHA / cognitive tests.

### Robust (WCAG 4.x)
- **4.1.1 Parsing.** Duplicate `id` attributes.
- **4.1.2 Name, role, value.** Empty `<button>` elements, unrecognized ARIA roles, unknown `aria-*` attributes.
- **4.1.3 Status messages.** Forms present but no `role="status"` / `aria-live` regions.

### WCAG 3.0 draft
- **Clear words / readable.** Flesch-Kincaid grade estimate from visible body text. The 3.0 draft "readable" outcome targets grade 9 or lower for general audiences.

### GSC-adjacent
- **Image Metadata (Google Search Console).** `ImageObject` JSON-LD missing `copyrightNotice`, `license`, `creditText`, `acquireLicensePage`, or using a plain string for `creator` instead of a Person / Organization object.

## How to fix every category yourself

### Missing or bad alt text (WCAG 1.1.1)

For every image, ask: "does this image communicate information that is not in the surrounding text?"

- If **yes** → `alt="Description of what the image conveys, not what it looks like."` Aim for 8–15 words.
- If **no** → `alt=""` (empty, not missing). Screen readers skip empty-alt images entirely, which is correct for decorative photos.
- **Never** use the filename (`alt="image1.jpg"`), a generic word (`alt="photo"`), or leave the alt attribute off.
- For images inside links, either put descriptive alt on the image OR put `aria-label` on the anchor. Do not leave the link unnamed.

### No skip link (WCAG 2.4.1)

Add this as the first interactive element inside `<body>`:

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

Then the CSS:

```css
.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: #000; color: #fff; padding: .75rem 1rem;
  z-index: 1000;
}
.skip-link:focus { left: 0; }
```

And mark your primary content area with `id="main"`.

### Generic link text (WCAG 2.4.4)

Every link must describe its destination when read out of context. Rewrite:

- "Click here to read our privacy policy" → "Read [our 2026 privacy policy](#)."
- "Read more →" → "[Read the full 2026 launch retrospective](#)."
- "More info" → "[See pricing and plans](#)."

If you truly cannot change the visible text (design constraint), add `aria-label="Read the 2026 privacy policy"` to the anchor.

### Missing form labels (WCAG 3.3.2)

Every form control needs a programmatically associated label. Three valid options:

```html
<!-- Option 1: <label for>-->
<label for="email">Email</label>
<input id="email" name="email" type="email">

<!-- Option 2: wrapped label -->
<label>Email <input name="email" type="email"></label>

<!-- Option 3: aria-label (visually-hidden cases only) -->
<input name="q" type="search" aria-label="Search site">
```

`placeholder="Email"` is **not** a label. It disappears on focus and fails WCAG.

### Duplicate IDs (WCAG 4.1.1)

`id` values must be unique in the document. Duplicates break:

- `aria-labelledby` / `aria-describedby` (picks the first match)
- `<label for>` (picks the first match)
- Skip links to `#content` (jumps to the first instance)
- CSS animations / JS selectors

Run `document.querySelectorAll('[id]')` through a `Map` counter in your browser console. Rename duplicates to `nav-search` / `main-search` / `footer-search`.

### Missing `lang` on `<html>` (WCAG 3.1.1)

One-line fix:

```html
<html lang="en">
```

Use the BCP-47 code that matches the primary language of the document: `en-US`, `es`, `ja`, etc. For mixed-language pages, add `lang` on the inline sections too: `<span lang="fr">Voilà</span>`.

### Viewport blocks zoom (WCAG 1.4.4)

Replace any `user-scalable=no` or `maximum-scale=1` in your viewport meta:

```html
<meta name="viewport" content="width=device-width, initial-scale=1">
```

That is the only correct viewport meta for responsive sites. Users must be able to pinch-zoom to 500% for partial-sight accommodation.

### Target size < 24×24 (WCAG 2.2 AA 2.5.8)

The target is the whole hit area, not just the visible icon. Fix with padding, not width:

```css
.icon-btn {
  padding: 12px;      /* pushes target to 24+24 */
  min-width: 24px;
  min-height: 24px;
}
```

Exceptions: inline text links, controls whose size is set by the user agent (a browser-native `<input type="date">` picker), and essential controls where the size is mandated by the presentation (tiny reposition handles in a design tool).

### Draggable without alternative (WCAG 2.2 AA 2.5.7)

If you reorder list items by dragging, expose move-up / move-down buttons too:

```html
<li>
  Task name
  <button aria-label="Move up">↑</button>
  <button aria-label="Move down">↓</button>
</li>
```

Users with motor impairments cannot drag. Voice-control users cannot drag. Keyboard users cannot drag.

### CAPTCHA on auth form (WCAG 2.2 AA 3.3.8 / 3.3.9)

The new "accessible authentication" outcomes prohibit cognitive-function tests during sign-in unless an alternative is offered. Swap out:

- reCAPTCHA v2 checkbox (fails because the fallback is image-puzzle grid).
- hCaptcha (similar issues).
- "Type the word you see" custom challenges.

Replace with email magic links, WebAuthn / passkeys, SMS one-time codes, or Cloudflare Turnstile (which does not require cognitive challenges for most traffic).

### Image Metadata fields (GSC non-critical issues)

If you publish `ImageObject` JSON-LD, Google Search Console flags these as "Image Metadata structured data issues":

```json
{
  "@context": "https://schema.org",
  "@type": "ImageObject",
  "contentUrl": "https://example.com/photo.jpg",
  "copyrightNotice": "© 2026 Josh Watte",
  "license": "https://creativecommons.org/licenses/by/4.0/",
  "acquireLicensePage": "https://example.com/licensing/",
  "creditText": "Photo by Josh Watte",
  "creator": {
    "@type": "Person",
    "name": "Josh Watte"
  }
}
```

Note `creator` is an object, not a bare string. `"creator": "Josh Watte"` triggers "Invalid object type for field creator" in GSC.

## The workflow I actually use

1. Run the [WCAG Accessibility Audit](/tools/wcag-accessibility-audit/) on the URL.
2. Click **Export CSV**, this is my working ticket list.
3. Click **Copy prompt** and paste into Claude with the rendered HTML of the page. The prompt produces BEFORE / AFTER HTML snippets per issue.
4. Apply the diffs, deploy.
5. **Revalidate against [wave.webaim.org/report](https://wave.webaim.org/report).** If WAVE still flags something our tool said was clean, WAVE wins. Open an issue on the repo so we can tighten the rule.
6. For anything that matters (checkout, sign-up, main content pages), do the final manual pass in a real browser with a screen reader. No automated tool catches focus-order errors, keyboard traps, or screen-reader-announcement quality.

## When automated testing is not enough

Every automated accessibility tool, WAVE, axe, Lighthouse, this one, catches somewhere between 30% and 55% of WCAG issues. The rest require human judgment. Things only a human can verify:

- **Computed color contrast at runtime.** CSS variables, dark mode, user preferences, and CSS filters all change contrast. Static HTML scanners cannot resolve any of them.
- **Focus order.** Does pressing Tab move through the page in a logical sequence? Does a modal trap focus correctly? Static HTML shows you the DOM order, not the visual reading order.
- **Screen-reader announcement quality.** Are your `aria-live` regions announcing the right thing at the right time? Does NVDA read your sign-in success message? VoiceOver?
- **Motion reduction.** Do your animations respect `prefers-reduced-motion`? The only way to know is to toggle the setting and look.
- **Keyboard traps.** Can you always Escape out of a modal? Can you reach every interactive element without a mouse?

Build automated audits into your pre-deploy CI. Build a quarterly manual audit, 30 minutes with NVDA, 30 minutes with a keyboard-only pass, 15 minutes in high-contrast mode, into your release cycle. That combination catches most real-user issues.

## Also on the site

The [Mega Analyzer](/tools/mega-analyzer/) now runs a parallel GSC product-schema + image-metadata check so the same issues show up in the single-page audit. The [Schema Validator](/tools/schema-validator/) has the full Google rich-results field set, including `shippingDetails`, `hasMerchantReturnPolicy`, and the IPTC image-rights fields, and will flag "creator as a string" the same way GSC does. The [Backlink Parser](/tools/backlink-parser/) now catches iframes injected by script (the case we missed on a storage-commander payment widget) and URLs embedded in inline scripts.

Ship the fix, rerun the audit, and validate against [wave.webaim.org/report](https://wave.webaim.org/report). That last step is non-negotiable.


---

Canonical HTML: https://jwatte.com/blog/blog-wcag-accessibility-audit/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-wcag-accessibility-audit.webp
