agentleFS
wormeymanwormeyman/wormeyman-skills/skills/accessibility-audit/SKILL.md

accessibility-audit

Skill4 starsChanged 11 days agoMITView on GitHub

Audit a web page, local file, or published artifact against WCAG 2.2 AA, then fix what it finds. Use whenever the user asks whether something is accessible, says "a11y", "WCAG", "ADA", "Section 508", "screen reader", "color contrast", "keyboard navigation", "tab order", or asks to run Lighthouse for accessibility - and also proactively right after building or restyling any page, because contrast and keyboard defects ship silently. Covers the theme matrix (one Lighthouse run only tests whichever theme the browser happens to be in), computed contrast sweeps, the checks Lighthouse does not run, and auditing published artifacts, which cannot be audited at their claude.ai URL.

55 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use design-system-audit, flaky-test-triage and 9 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • jpoindexterSkill

    design-system-audit

    Audit code for design system violations - grid spacing, tokens, existing components

    Also in 4 projects that use this

  • SUDARSHANCHAUDHARISkill

    flaky-test-triage

    Diagnoses inconsistent test results by separating product nondeterminism, automation defects, shared data, timing, infrastructure, environment, order, and concurrency effects.

    Also in 3 projects that use this

  • tekumaraSkill

    api-design-review

    Reviews REST API design by inventorying routes, finding overlapping functionality, comparing payloads and pagination, and checking resource naming, URL structure, identifiers, errors, idempotency, and timestamps. Use when asked to review API routes, compare endpoints, find redundant or superseding operations, assess contract consistency, or run a REST API design review.

    Also in 3 projects that use this

  • BufferoverflovvSkill

    observability-setup

    Add production observability to a FastMCP server: request IDs, slow-request logging, structured log fields, and optional Prometheus metrics. Use when: hardening logging for production, adding request tracing or correlation IDs, setting up slow-request alerts, adding a /metrics endpoint, or improving structured log output beyond the base middleware.

    Also in 3 projects that use this

  • mmondoraSkill

    infrastructure-as-code

    Infrastructure as Code with Terraform and Pulumi. State management, modularity, drift detection, secrets handling. Use when provisioning cloud resources, writing Terraform/Pulumi modules, or managing infrastructure configuration.

    Also in 3 projects that use this

  • OpenAISkill

    design-qa

    Internal prototype QA helper. Use only after a Product Design prototype, URL-to-code build, or image-to-code build has a source visual target and a rendered implementation to compare before handoff. Do not use for broad UX critique, design critique, product audits, or flow reviews; route those user-facing requests to audit.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

7 rules. 0 are common, 4 are distinctive, and 3 are very specific to this project.

  • 0

    Common

    Most of the 46 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If a page has forms, modals, or heavy ARIA, run that skill too.

    line 22
  2. Very specific:

    Run it with run_in_background: true, and kill it when finished.

    line 55
  3. Distinctive:

    Instead, wrap your source file in the same skeleton the publisher applies and serve that locally:

    line 59
  4. Distinctive:

    Never a single run. Minimum three:

    line 75
  5. Very specific:

    Never read report.json raw.

    line 154
  6. Very specific:

    If width reads 980 instead of 320, the emulation worked and the page has no <meta name="viewport">.

    line 195
  7. Distinctive:

    If a script-detectable defect is missed, the skill has regressed.

    line 251

“Common” is measured: kept by at least 6 in 10 of the 46 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Accessibility Audit

Audit a page, report findings with severity, fix on approval, then re-run to prove the score moved.

Relationship to a11y-debugging

The chrome-devtools plugin ships an a11y-debugging skill. It is good and this does not replace it. Split the work:

Use a11y-debugging forUse this skill for
Reading the accessibility treeThe theme matrix
Form labels, orphaned inputsComputed contrast sweeps
ARIA attributes and rolesChecks Lighthouse omits entirely
Focus traps in modalsAuditing published artifacts
Per-element tap-target checksThe fix-and-verify loop

If a page has forms, modals, or heavy ARIA, run that skill too. Do not re-derive its material here.

The one rule that matters most

Lighthouse only ever audits the theme the browser is currently rendering. It does not test both. A theme-aware page audited once has been half audited, and the half you skipped is usually the worse one.

Two measured examples, both reproducible:

A real page scored 87 in dark mode with zero contrast findings. Forced to light, the same page scored 77 with 41 failing elements. The single run was not a smaller version of the truth. It was the wrong answer.

Worse, the score does not have to move at all. references/fixture.html scores 62 in both themes - and the contrast failures behind that identical number do not overlap at all:

Elements Lighthouse flags
Darkp.dark-fail, p.fade-fail, span.status.good
Lightp.light-fail, p.fade-fail, and four <th>

Fix everything the dark run reports and the page still has five contrast failures in light, still scoring 62. A stable score across themes is not evidence the themes are equally sound. Always run the matrix in Step 2.


Step 1 - Get an auditable URL

Three cases. Pick the right one before running anything.

A live site. Audit the URL directly. Nothing to set up.

A local file. Serve it over HTTP. Never audit file:// - Lighthouse behaves differently and some audits silently skip.

cd <dir> && python3 -m http.server 8731 --bind 127.0.0.1

Run it with run_in_background: true, and kill it when finished.

A published artifact. This is the case people get wrong. Pointing Lighthouse at https://claude.ai/code/artifact/<uuid> audits the claude.ai wrapper page, not your content. The score is meaningless.

Instead, wrap your source file in the same skeleton the publisher applies and serve that locally:

<!doctype html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<style>*{margin:0;padding:0}</style></head>
<body>
<!-- your artifact source file, verbatim -->
</body></html>

Build a second copy with <html lang="en">. You author body content, not the <html> tag, so html-has-lang is the publisher's to fix and it costs 7 points. Auditing both copies separates your score from theirs - report the with-lang number as your content's true score and name the gap.

Step 2 - Run the theme matrix

Never a single run. Minimum three:

RunSetup
Desktop lightemulate with colorScheme: "light", then lighthouse_audit
Desktop darkemulate with colorScheme: "dark", then lighthouse_audit
Mobilelighthouse_audit with device: "mobile" - reflow and tap-target rules only run here

emulate persists across navigations, so set it once per theme and run everything for that theme before switching.

Running it from the CLI instead

lighthouse_audit needs the chrome-devtools MCP. When you do not have it - CI, a git hook, a headless box - the CLI does the same job:

npx -y lighthouse@latest "$URL" \
  --only-categories=accessibility \
  --output=json --output-path=./report.json \
  --chrome-flags="--headless=new --blink-settings=preferredColorScheme=1" \
  --quiet

Feed report.json straight to the extractor in references/scripts.md.

The default theme is dark, and almost nobody expects that. A single npx lighthouse run with no theme flag audits the dark half and reports a number that reads like the whole answer. Measured on Lighthouse 13.4.1 with --headless=new, against a page built to fail contrast only in dark:

--chrome-flagsPaintedScore
(none)dark84
--blink-settings=preferredColorScheme=0dark84
--blink-settings=preferredColorScheme=1light100
--blink-settings=preferredColorScheme=2 or =3light100
--force-dark-modedark84

So the enum is 0=dark, 1=light, anything above clamps to light. Every dark run painted the page's own @media (prefers-color-scheme: dark) colours, so these set the real media query rather than Chrome's auto-darkening. Always pass the flag explicitly, both themes, rather than leaning on a default that is not what you would guess and is free to change.

This inverts the usual advice. lighthouse-ci#1022 is the canonical thread on testing dark mode, and it closed without a working answer - Sec-CH-Prefers-Color-Scheme, --force-dark-mode and --enable-features=WebContentsForceDark were all reported as no-ops. It was written when dark was the hard one to reach. Today dark is free and light is the one you have to ask for. Do not follow that thread's advice; measure instead. WebContentsForceDark in particular is Chrome repainting a light page by algorithm, which is not the dark theme your users see.

For a true 320px reflow run, the CLI takes the width directly, which is more reliable than resizing a window:

--screenEmulation.mobile --screenEmulation.width=320 \
  --screenEmulation.height=800 --screenEmulation.deviceScaleFactor=2

Confirm it landed rather than assuming - the report records what was used:

json.load(open("report.json"))["configSettings"]["screenEmulation"]["width"]   # 320, not 412

Keep the MCP for the matrix when you have it. The CLI cannot run script in the page, so while it can prove the viewport it used, it cannot prove which theme it actually painted - there you are trusting a flag. With emulate plus the contrast sweep you read matchMedia back in the scheme field and catch an emulation call that silently did not take. Use the CLI for CI, for viewport precision, and when there is no MCP; use the MCP when you need the run to prove what it tested.

Gotchas that cost real time:

  • A run reporting 0 across every category, with 0 passed and 0 failed, is a failed run, not a score. Nothing was measured. Check runtimeError in the JSON and retry before you conclude anything. Live sites behind a WAF or CDN throw this: a real audit of a production WordPress site returned ERRORED_DOCUMENT_REQUEST with a 403 on the first attempt and scored 96 on an immediate retry, with no change to anything. The distinction matters because a genuine 0 and a failed run look identical in the summary, and only one of them is worth reporting.
  • lighthouse_audit rejects outputDirPath outside the configured workspace roots. Omit it and take the temp path from the result, then copy the HTML report somewhere durable if you want to keep it.
  • The result summary's "Failed: N" counts every category, not just accessibility. Read the JSON for the real accessibility failures.
  • Never read report.json raw. It runs to hundreds of KB. Extract with the script in references/scripts.md.

Step 3 - Sweep contrast by computation

Lighthouse samples. It also cannot see a failure in the theme it did not render. Compute contrast yourself against what the browser actually painted.

Run the contrast sweep from references/scripts.md via evaluate_script, once per theme. It walks every text-bearing element, resolves the real effective background by climbing ancestors until it finds a non-transparent one, and applies the correct threshold for that element's computed size and weight (3:1 for large text, 4.5:1 otherwise).

It reports exact ratios, which is what you need to pick a replacement color.

It also composites opacity. opacity fades the painted text toward its background and leaves getComputedStyle(el).color completely unchanged, so a sweep that reads the computed colour reports a clean pass on text that fails. Measured on a page already scoring 100 in both themes: an opacity: .82 label put three light-theme cases under 4.5:1, worst 3.78, with the sweep still reporting zero failures. Check the alpha field on any finding - below 1 means the fade is part of the cause. This applies to your own fixes too: dimming a newly added label is the easiest way to introduce a failure while the tooling insists nothing changed.

Lighthouse's contrast audit (axe) also composites opacity, so neither tool is better at everything. The sweep adds exact ratios, every element instead of a sample, and whichever theme you point it at. When the two disagree, find out which is right before believing either.

If you are also authoring the page, check the palette before building. Contrast is far cheaper to fix in six token values than in 41 rendered elements. Run the token pre-check from references/scripts.md against your palette, in both themes, before writing components. Test each foreground against the darkest light surface and the lightest dark surface - those are the worst cases, and a token that clears white may still fail on a tinted panel.

Step 4 - Run what Lighthouse does not

These are real WCAG failures with no Lighthouse audit behind them. Run the structural check from references/scripts.md, then verify by hand:

  • Keyboard access to scrollable regions. Any overflow-x: auto container holding a wide table or code block is unreachable by keyboard without tabindex="0". Lighthouse does not run axe's scrollable-region-focusable rule at all.
  • Table header semantics. th without scope, and first-column cells marked up as td when they are functionally row headers. Lighthouse's table audits return "not applicable" and tell you nothing.
  • Color as the only signal. Status shown by red text alone, a colored rail with no text label, a chart series distinguished only by hue. Run the polarity probe from references/scripts.md, then read what it surfaces. The trap is not the empty cell, it is the full one. +$1,000 and +$130 are both complete, specific, correctly formatted values, and on a real page the first was good news and the second was bad, distinguishable only by being green or red. Asking "what does a colorblind user lose here" answers "nothing, the numbers are right there" and you move on. Ask instead whether the text alone reveals which direction is better. A signed number, a bare percentage, a date, a name: none of them carry polarity.
  • Two colors encoding opposite states. Run the status-pair luminance check from references/scripts.md. Contrast tools compare each color to its background; both can pass and still be the same brightness as each other, leaving hue as the only difference. Greyscale and color vision deficiency both erase hue.
  • Focus visibility. Tab through interactively. Anything focusable needs a visible :focus-visible state, including containers you just gave tabindex="0".
  • Reflow. At 320px wide, the page body must not scroll sideways. Wide content scrolls inside its own container. Use emulate to get to 320px, not resize_page - resize_page sets the browser window, which has a floor around 500px on macOS and silently gives you a 500px viewport while reporting success. Ask the page what it actually got:

    // set BOTH in one call - see below
    emulate({ viewport: "320x800x2,mobile,touch", colorScheme: "light" })
    // then confirm, before trusting anything measured at this size
    evaluate_script(() => ({ width: innerWidth,
      scheme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
      sideways: document.documentElement.scrollWidth > document.documentElement.clientWidth }))

    A run that believes it tested 320 and actually tested 500 will pass a page that breaks on a real phone.

    Each emulate call resets every setting it leaves out. Measured: emulate({colorScheme:"light"}), then emulate({viewport:"320x800x2,mobile,touch"}), and the page read back scheme: "dark". The reflow run had silently changed theme, so it was a dark 320px run wearing a light label. It works the other way too: a later call with only colorScheme puts the viewport back to the window's width. So pass every setting you need in each call, and read width and scheme back every time.

    If width reads 980 instead of 320, the emulation worked and the page has no <meta name="viewport">. Phones will show it as a shrunken desktop page. That is a finding, not a failed run.

Step 5 - Report

Group by severity, and state clearly which findings are yours to fix versus the host's. A score with an unexplained cap is not useful information.

Report per theme. "95 in both themes" and "95 light, 77 dark" are very different results.

If the page ships no prefers-color-scheme rules at all, say the matrix was confirmatory. Plenty of sites have one theme. Reporting three identical runs as though they were three independent findings overstates the work and buries the real ones. Say the site has a single theme, that you verified it rather than assumed it, and move on. The structural check reports prefersDarkSupported for exactly this call - but treat a false there as a prompt to confirm by running the other theme, never as permission to skip it.

Watch the denominator on mobile. Mobile runs fewer audits than desktop, so a one-point gap between them is usually arithmetic rather than a defect. Compare the failure lists, not the scores, before reporting a difference.

Step 6 - Fix, then verify

Apply fixes, then re-run the full matrix. A fix verified in one theme is not verified.

State the before and after numbers per theme. If a number did not move, say so rather than implying it did.

Red flags

ThoughtReality
"Lighthouse says 95, we're good"In which theme? Run the matrix.
"The contrast audit passed"It only checked the rendered theme, and it samples. Sweep it.
"The sweep reported zero failures"Does it composite opacity? A faded label fails while computed colour looks fine.
"I resized the window to 320px"resize_page floors near 500 on macOS. Use emulate and read back innerWidth.
"I ran npx lighthouse and it scored 95"That was the dark theme. The CLI defaults to it. Pass preferredColorScheme both ways.
"The polarity count didn't move, so the probe is broken"Or your label has no leading space and really is one word. Read an element's textContent.
"I'll audit the artifact URL"That audits claude.ai's wrapper. Serve it locally.
"No tables or forms, so semantics are fine"Scroll containers and heading order still fail silently.
"It's a static page, keyboard access is moot"Scrollable regions are interactive whether you intended it or not.
"I'll read the Lighthouse JSON to find failures"It is hundreds of KB. Use the extractor.
"Score went 77 to 95, done"Re-run every theme. And name what caps the remaining 5.
"It scored 0, the page must be broken"0 with nothing passed and nothing failed means the run failed. Check runtimeError and retry.
"Desktop 96, mobile 95, so mobile is worse"Mobile runs fewer audits. Compare failure lists, not scores.

Verifying this skill still works

references/fixture.html has eleven planted defects and a manifest in its source comment. Serve it and run the full workflow against it.

Expected result. Ten of the eleven are caught by script:

Found inDefect
Light run only.light-fail contrast, 2.75:1
Dark run only.dark-fail contrast, 2.03:1
Both runsno <main>, no lang, H1 H3 heading skip, missing alt
Both runsscroll region with no tab stop and no label
Both runstable with no caption and four th with no scope
Both runs.fade-fail at 2.45:1 light and 3.71:1 dark - only if the sweep composites opacity
Focus probelink with outline:none and no replacement

Defect 11 catches a sweep that does not composite opacity. Its token is redefined for both themes and clears 4.5:1 on either ground at full strength, so getComputedStyle reports a passing colour in both runs. Only compositing the opacity reveals it. A run that finds defects 1 and 2 but reports the page clean of 11 has a sweep that skips opacity, which is the regression this defect exists to catch.

Defect 10 - state carried by color alone in the status column - has no definitive check. The polarity probe surfaces it as a candidate (span.status.good, text "caught", which does not reveal whether being caught is good or bad), alongside two false positives from .light-fail and .dark-fail, whose class names describe the planted defect rather than a UI state. Three candidates, one real. That ratio is the point: the probe generates leads, you decide. If a run reports the fixture clean on defect 10 without looking at the candidates, that is the failure the manual pass in Step 4 exists to prevent.

If a script-detectable defect is missed, the skill has regressed. Fix the skill, not the fixture.

References

  • references/scripts.md - contrast sweep, token pre-check, structural check, Lighthouse JSON extractor
  • references/fixes.md - finding-to-fix patterns
  • references/fixture.html - page with planted defects, for verifying this skill still works

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.