← Back to Blog

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

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

I added a new tool to the hub: the 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 actually 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. 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 very 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 actually 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. 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 yesalt="Description of what the image conveys, not what it looks like." Aim for 8–15 words.
  • If noalt="" (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>:

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

Then the 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:

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:

<!-- 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 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:

<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:

.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:

<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":

{
  "@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 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. 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 the vast majority of real-user issues.

Also on the site

The 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 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 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. That last step is non-negotiable.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026