WordPress accessibility remediation: Claude and Checker Pro

Illustrated zine-style collage titled “WordPress Accessibility Remediation.” Six diverse collaborators, including a wheelchair user, gather around a laptop displaying an accessibility audit. Comic panels and handwritten notes trace 557 findings to 16 root causes and zero open issues, followed by a warning that 779 findings returned. Surrounding details highlight captions, color contrast, code testing, and the importance of measuring accessibility improvements.

WordPress accessibility remediation went faster on my 2008 site once Claude grouped 557 open findings into 16 root causes and fixed each one at its source. It went wrong whenever Claude assumed instead of measuring. On the morning of my talk, the live site showed 779 open findings again. Here’s the method, the mistakes, and what I’d build first.

This post accompanies my talk on WordPress accessibility remediation for the WordPress Accessibility Meetup, hosted by Amber Hinds. Every claim links to the standard, spec, or source behind it, so you can check my reasoning instead of taking my word for it.

Using Accessibility Checker Pro + Claude AI to Remediate a Large WordPress Site, Tue, Oct 20, 2026, 8:00 AM | Meetup

Meetup

Using Accessibility Checker Pro + Claude AI to Remediate a Large WordPress Site, Tue, Oct 20, 2026, 8:00 AM | Meetup

Courtney Robertson will share a practical case study of running Equalize Digital’s Accessibility Checker Pro across her long-running WordPress site (including a large archi

🗂️ The site and the starting point

courtneyr.dev has run on WordPress since 2008. It has 317 published posts, a custom child theme, and a large archive of 2008 to 2013 posts. I recently moved hosts, and the September remediation happened alongside that move. My own accessibility statement says what I commit to, so the site needed to match it.

I ran Accessibility Checker with the Pro plugin from Equalize Digital, and gave Claude Code shell access to the site through WP-CLI. I’d already written about how I choose AI tools for editing this site. The baseline, taken on September 24, 2026:

  • 5,095 rows in the Checker’s table, 557 of them open
  • 11 distinct rules across 324 posts and pages
  • 546 “Needs Review” warnings and 11 “Problems”

🛠️ How I approached WordPress accessibility remediation

  1. Read the table, not the dashboard. I exported every open row as JSON through WP-CLI instead of working from the Issues Explorer screen.
  2. Cluster by rule plus element. 557 rows became 16 root causes. A cluster is a better unit of work than a row, because one template usually emits a hundred of them.
  3. Fix at the emitter. Claude changed the theme filter, pattern, or plugin that produced the markup before touching content. Content edits came second. An automatic ignore rule came last, and only for review prompts with nothing to fix.
  4. Rescan, then reconcile per row. Claude matched baseline rows to new rows by post, rule, and element signature, never by row id, because a Pro scan deletes and reinserts rows.
  5. Keep my own rules. No bulk dismissals to reach zero. Every dismissal carries evidence or my decision, word for word. Nothing writes to the live site unless I run it.

By the September 25 scan, the 557 baseline rows resolved to 397 fixed and 160 dismissed with evidence. Zero were open. I took a victory lap. It was premature.

🎬 Video: from YouTube embeds to Able Player with transcripts

Why it matters. Captions let people who are deaf or hard of hearing follow a video. A transcript also serves people who read with a braille display and anyone who can’t play audio. WCAG covers this across several criteria: 1.2.1 Audio-only and video-only, 1.2.2 Captions, 1.2.3 Audio description or media alternative, and 1.2.5 Audio description.

🔎 The first problem

In June the Checker flagged 45 “Missing Transcript” findings, and 41 were Able Player embeds that already had captions. Able Player builds its interactive transcript in JavaScript from a WebVTT file. The Checker’s Missing Transcript rule looks for the word “transcript” within 25 characters of the media. It can’t see a transcript that a script builds later.

💡 A shortcode tip that cost me a morning

In the Able Player WordPress plugin, captions="ID" puts an uploaded track under the CC button. subtitles="ID" doesn’t. We chased the wrong cause first, and found the right one by testing it. Joe Dolson, who maintains the plugin, also added an option in version 2.4.0 to replace YouTube embeds on the front end. Read the commit and the changelog to see what it does and doesn’t cover.

⚠️ The mistake in my own workaround

My first fix automatically ignored a video whenever YouTube had a caption track. When we audited my dismissals again in September, we found 21 videos with only YouTube’s automatic captions. Those don’t satisfy 1.2.2, which requires captions that are accurate. Twenty three rows reopened.

🎙️ The caption pipeline

For the 21 videos, yt-dlp fetched the audio and OpenAI Whisper transcribed it locally. Each WebVTT file carries a NOTE saying it’s an unreviewed machine transcript. The theme attaches the file to every player for that video, so no post content changed. That’s about 18 hours of audio, transcribed in 9 hours and 15 minutes. The site now has 80 videos with synchronized tracks.

🚧 What’s still open

The Whisper transcripts are unproofread. I haven’t reviewed audio description per video. One post, a song, is transcript only by my decision.

🔁 The vendor loop, and the test I ran

Amber opened issue 1740 in June asking Accessibility Checker to recognize Able Player transcripts. Equalize merged PR 1741 on October 1 and shipped it in version 1.51.0 on October 9. That’s four months from report to release.

I tested it before retiring my workaround. Claude ran the 1.51.0 scanner and my current 1.49.0 scanner on four pages, with and without my figcaption. With the figcaption removed, both versions still flagged Missing Transcript on two pages. That was a headless run of the free scanner, with a 2.5 second wait, on four pages. I haven’t run a full site scan with 1.51.0, so I can’t tell you why. If your results differ, tell me what you see.

🧩 aria-hidden: 492 findings from a few patterns

Why it matters. aria-hidden removes an element from the accessibility tree. That’s correct for decoration and harmful for content, so the Checker asks for a review each time. See also MDN’s reference. The related WCAG criteria are 1.1.1 Non-text content, 1.3.1 Info and relationships, and 4.1.2 Name, role, value.

One pattern, 323 rows. The footer h-card photo sat beside the author link with alt="" and aria-hidden. Moving the photo inside the named link, with alt="", removed the need for aria-hidden. One template change cleared 323 rows.

A conflict between two rules. The Checker’s empty alt rule passes alt="" only with aria-hidden. Its aria-hidden rule then flags that same image. The W3C Nu validator rejects the role="presentation" workaround on an image with empty alt. No valid markup satisfied both, so 104 rows were dismissed with evidence for each instance, such as the commenter’s visible name beside each avatar. For deciding what counts as decorative, the W3C alt text decision tree and WebAIM’s alt text guide helped.

A scanner artifact. Six rows pointed at <html>. Able Player creates SVG icons that it never attaches to the page, and the scanner reported them as the document element. A node that isn’t in the page isn’t in the accessibility tree, so those are false positives.

Emoji in avatar alt text. Fediverse display names arrive with custom emoji shortcodes like :prami_pride:. Those became avatar alt text, and the Checker read the underscore as a filename. The theme now strips shortcodes when a comment arrives.

🎨 Color contrast, in both color modes

Why it matters. Low contrast makes text hard to read for people with low vision, and for everyone in bright light. 1.4.3 Contrast (Minimum) sets 4.5:1 for normal text. The WebAIM contrast checker tests a pair in seconds.

  • Real failures: table header cells measured 2.45:1 because a band color rule outranked th h3 { color: inherit }. Setting the color tokens to currentcolor fixed every path at once.
  • Dark mode needs its own scan. The Checker has no handling for prefers-color-scheme, so one scan covers one mode. The dark scan found 4 more elements, including two old inline highlight spans at 1.02:1. Claude replaced them with <mark>, which the theme styles for both modes.
  • A false positive. The scanner measured hero text against the cover image behind it, though the text sits 19 pixels below the image, on white. The text measured 15.67:1. We dismissed the row with the number.

🕳️ A scan is only as good as its coverage

597 of 927 pages had never been scanned. Perfmatters’ Delay JavaScript blocked the scripts the Checker injects, so the Checker failed to scan every archive page. The totals looked clean. A clean report is easy to produce when 597 pages aren’t in it. If you run a full site scan, read its failure list before you read its totals.

⏪ The regression: 779 findings on the morning of the talk

In September I had zero open. On the morning of this talk, my live site’s Checker table showed 779 open rows. The site had been busy. 711 of them were one element on 711 posts: the visual copy of the cut paper archive title.

The title already printed its text in a sibling that only screen readers get. The Checker passes an aria-hidden element when a sibling’s class names screen reader text, and my theme used a different class name. Adding the standard class cleared it. I committed the fix, and it’s running on my dev site, with a Playwright test covering it. I haven’t deployed it yet.

That’s the h-card case again. The cause, though, is new: design work shipped after the WordPress accessibility remediation, with no check to catch it.

🤦 Where Claude got it wrong

Here are the ways the project embarrassed both of us:

  • It assumed YouTube’s automatic captions satisfied 1.2.2. My second audit read each video’s caption track type and caught it.
  • It emptied the alt on linked images to clear a “redundant alt” warning, which created a Critical error. Removing the duplicate image fixed it.
  • 110 of 8,411 older dismissals concealed real defects, such as alt text that said “IMG_1159” and empty paragraphs. Older Bulk Ignore and Global Ignore actions, some from my own earlier passes, had covered them up. The second audit found them.
  • A clean report with 597 pages unscanned. See above.
  • GIF alt text described the wrong clips. Looking at the actual frames caught it.
  • Predictions reported as results. We’d listed 141 rows as dismissed before the scan had written them.
  • It expected the 1.51.0 fix to clear my pages. Testing showed otherwise.

Delegation needs the same discipline. This morning I had Claude hand two side tasks to helper agents. I reran their lint and tests before relying on their reports.

🏗️ Building it accessible from the start

WordPress accessibility remediation is a cleanup, and cleanups go stale. Here’s the order I’d build in next time.

  1. Defaults. The first thing a developer types should already pass. The first rule of ARIA is to use a native element when one exists, and the ARIA Authoring Practices guide explains why. A shared component library makes the accessible choice the default. The giffgaff talk at LeadDev is a good case study, though the team reported its own figures.
  2. Gates. A gate refuses inaccessible output. Block validations, checks that run before a commit, and the required items in The Website Specification all work this way. I had Claude build a Playwright test that runs the Checker’s own scanner over 11 templates and fails on any element I haven’t reviewed. Its first run on my dev site failed with 10 findings nobody had looked at. That’s the gate working.
  3. Human review. A passing score is a floor. In Helen Hou-Sandí’s WordPress Accessibility Day talk, axe-core finds about 25% of the issues GitHub’s audits flag. The talk also covers GitHub’s open source accessibility scanner.
  4. AI-generated code. In the WordPress Accessibility Day session, I reviewed this and prompted fixes for attributes such as contrast and labels. It didn’t fix keyboard and focus behavior. Point your manual testing at menus, dialogs, tabs, and carousels.

🎞️ A known gap: looping GIFs

I kept eight looping GIFs and added visible descriptions. That satisfies alt text, but not 2.2.2 Pause, Stop, Hide, a Level A criterion for motion that starts automatically and lasts over five seconds. I documented it as an exception. I’ve since had Claude build a click to play control that respects prefers-reduced-motion, has a 24 pixel minimum target, and shows a visible focus ring. It passes tests on a fixture page. It hasn’t been tested on the real posts or with a screen reader, so it isn’t live.

✅ Takeaways from WordPress accessibility remediation

  1. Fix at the emitter. One pattern cleared 323 rows, and one class name cleared 711.
  2. Measure the claim: ratios, cue counts, focusable descendants.
  3. Every dismissal carries evidence, and old dismissals get audited again.
  4. A clean scan is only as good as its coverage and the color mode it ran in.
  5. Remediation regresses. A gate keeps it fixed.
  6. Test a vendor’s fix on your own pages before you retire your workaround.

If you try the same four page comparison during your own WordPress accessibility remediation, send me what you find.

📚 Tools, standards, and further reading

Standards and definitions

WordPress

Tools I used

My public repositories

Thanks to Amber Hinds and the Equalize Digital team for the work on issue 1740, and to Joe Dolson for Able Player.

FIELD NOTES

Get the next field note in your inbox

A weekly note from the open-source frontier — what I’m building, breaking, and learning. No spam, no AI summaries, real writing from a real human.

Comments

2 responses

  1. On the morning of my talk, the live site showed 779 open findings again. Here’s the method, the mistakes, and what I’d build first. This post accompanies my talk on WordPress accessibility remediation for the WordPress Accessibility Meetup, hosted by Amber Hinds.

Leave a Reply

Your email address will not be published. Required fields are marked *

To respond on your own website, enter the URL of your response which should contain a link to this post’s permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post’s URL again. (Find out more about Webmentions.)