Audit the page with the Chrome DevTools MCP. Work through the steps in order.
If the user did not name a target, assume WCAG 2.2 Level AA and say so in the report.
If the Chrome DevTools MCP tools are not available, stop and tell the user to install it rather than substituting a scripted browser. Hand-rolled probes are where false positives come from.
The server is chrome-devtools-mcp, launched with npx -y chrome-devtools-mcp@latest. Registration differs per agent, so point the user at their own MCP configuration rather than naming a command.
Register it with its full tool set. --slim exposes three tools and drops take_snapshot and list_console_messages, which this skill depends on.
1. Baseline
navigate_pageto the URL.list_console_messageswithtypes: ["issue"]andincludePreservedMessages: truefor Chrome's own accessibility issues.- Run
scripts/axe-run.jsthroughevaluate_script. This is the cheapest high-value pass and catches most mechanical failures.
Report incomplete separately from violations: those are "needs review", not failures, and they are where the real findings usually hide.
Repeat for each page type that matters (home, listing, detail, form, checkout). One page is not an audit.
2. Structure and semantics
take_snapshotfor the accessibility tree.- Heading hierarchy: one
h1, no skipped levels, no empty headings. - Landmarks present (
main,nav,header,footer) and each repeated landmark uniquely named. <title>present, descriptive, free of typos.html[lang]set.- Compare snapshot order against
take_screenshotto confirm DOM order matches visual reading order. - Resolve every
aria-labelledbyandaria-describedbytarget. A dangling or empty reference leaves the element unnamed and axe only reports it as incomplete.
3. Labels, images, forms
From the snapshot, confirm buttons and links have a non-empty accessible name, and that images have meaningful alt (or alt="" when decorative).
Read names from the accessibility tree, not by hand-assembling attributes. A control can be named by a nested role="img" with aria-label, by an <svg><title>, or by aria-labelledby. Reconstructing that yourself produces phantom "unnamed control" findings.
Check autocomplete on every field (SC 1.3.5, and SC 3.3.8 for credentials) with scripts/form-fields.js.
autocomplete="off" on username or password fields fails SC 3.3.8. Generic labels such as "Form field" fail SC 3.3.2.
4. Keyboard and focus
Drive the keyboard with the MCP's own click and press_key. Never open a menu or dialog with element.click() inside evaluate_script. A synthetic click does not focus the trigger, so the component captures document.body as the element to restore, and the page appears to lose focus on close when it does not. That is a bug in the test, not the page.
- Tab through and confirm focus order is logical and the indicator is visible (
take_screenshot). - Confirm no keyboard traps, and that Escape closes overlays and returns focus to the trigger.
- For dialogs, check what kind it is before judging it:
const d = document.querySelector('dialog');
return { modal: d?.matches(':modal'), open: d?.open };
A native <dialog> opened with showModal() already provides focus trapping, background inertness and top-layer placement. Do not report missing aria-modal or a missing inert attribute for one. Adding aria-modal there is redundant and discouraged. Check the accessibility tree for modal: true and for the dialog's name, which is the part that genuinely does get missed.
5. Colour contrast
Trust axe's color-contrast result first. When resolving a value yourself, never parse the string returned by getComputedStyle. Modern themes use oklch() and color-mix(), which are returned verbatim, and reading the numbers out of them as RGB produces nonsense ratios. Use scripts/contrast.js, which resolves through a canvas instead.
Composite semi-transparent backgrounds against what is actually behind them. For text over a photo, sample the image's darkest and lightest pixels and check the worst case.
Also check SC 1.4.11 Non-text Contrast at 3:1, which axe does not cover at all: form control borders, focus rings, icon-only buttons and toggle states.
6. ARIA
- No roles duplicating native semantics (
<button role="button">). - No
aria-labeloraria-labelledbyon a generic<div>or<span>, where naming is prohibited and the name is discarded. - No
aria-hiddenon focusable elements. - Before flagging an
aria-liveregion as noisy, checkaria-atomicandaria-relevantand whether anything announceable actually changes. With both unset, only added nodes and text changes announce, never the whole region, and a CSS scroll-snap carousel changesscrollLeftrather than the DOM. Confirm with aMutationObserverover a real interaction before reporting.
7. WCAG 2.2 additions
Automation covers almost none of these. Check each explicitly and say which passed.
| Criterion | Level | How to check |
|---|---|---|
| 2.4.11 Focus Not Obscured (Min) | AA | Tab while scrolled and confirm no sticky or fixed element covers the focused control |
| 2.5.7 Dragging Movements | AA | Every drag action has a single-pointer alternative |
| 2.5.8 Target Size (Min) | AA | 24x24 CSS px, or the spacing exception below |
| 3.2.6 Consistent Help | A | Help or contact mechanism in the same relative order on every page |
| 3.3.7 Redundant Entry | A | No information asked for twice in one process |
| 3.3.8 Accessible Authentication (Min) | AA | Password managers can fill: autocomplete set, paste not blocked |
For SC 2.5.8, a target under 24x24 still passes if a 24px diameter circle centred on it intersects no other target's circle. Measure centre-to-centre distance before reporting, since most undersized targets pass this way. Note when the margin is thin so it does not regress later.
Check reflow (SC 1.4.10) and resize (SC 1.4.4) with resize_page to 320px wide and confirm no horizontal page scroll. An internally scrolling carousel is allowed.
8. Verify before reporting
Re-test every finding by a second, different method before it goes in the report. A DOM attribute reading is not evidence of runtime behaviour. Most false positives come from inferring behaviour from static markup rather than observing it.
State what you did not test. No screen reader, no authenticated pages, no alternative themes.
Close the page with close_page when done.
Report
Group by severity: Critical (blocks access), Serious (significantly impairs), Moderate (confusion or extra effort), Minor.
For each issue give the criterion and level, the selector or element, the measured evidence, and the fix. Mark anything that passes under WCAG 2.1 but fails under 2.2 explicitly, since that is the usual reason for a re-audit.
List what was verified as passing too. A bare list of failures tells the reader nothing about coverage.
Keep it to the findings. Audit process narration belongs in the conversation, not the report.