WCAG 2.2 Audit Suite for Headless Chrome
Overview
A zero-dependency Node.js command-line tool that renders pages in headless Chrome over the DevTools protocol and measures WCAG 2.2 problems rather than guessing from markup. Ten checks: text contrast against the background actually painted behind the text (translucent layers composited, opacity applied), clipping under the 1.4.12 text-spacing overrides and at 200% default text size, reflow at 320px with target size including both 2.5.8 exceptions, controls that lose their edge in forced colours, invisible or obscured focus, a keyboard walk that catches traps, missing focus indicators and scripts that throw focus away, unnamed controls and images from the accessibility tree, broken id references, and motion that ignores reduced-motion. One command gives a console report with a CSS selector per finding, a JSON report and CI exit codes, including a separate code when a page failed to load or a built-in canary was missed, so a broken run is never reported as clean. Tested only on Windows 11 with Node 24.19 and Chrome 154. It is not a conformance audit: warnings need human review.
Source preview
let bg = null, painted = '';
for (const node of (idx >= 0 ? stack.slice(idx) : [el])) {
const ncs = getComputedStyle(node);
if (ncs.backgroundImage && ncs.backgroundImage !== 'none') { painted = 'background-image'; break; }
const c = parse(ncs.backgroundColor);
if (c && c.a > 0) { bg = bg ? over(bg, c) : c; if (bg.a >= 0.99) break; }
}
if (!painted && (!bg || bg.a < 0.99)) {
for (let node = el.parentElement; node; node = node.parentElement) {
const ncs = getComputedStyle(node);
if (ncs.backgroundImage && ncs.backgroundImage !== 'none') { painted = 'background-image'; break; }
const c = parse(ncs.backgroundColor);
if (c && c.a > 0) { bg = bg ? over(bg, c) : c; if (bg.a >= 0.99) break; }
}
}
// Nothing opaque anywhere: the canvas behind the page is white.
if (!painted && (!bg || bg.a < 0.99)) bg = bg ? over(bg, { r: 255, g: 255, b: 255, a: 1 }) : { r: 255, g: 255, b: 255, a: 1 };
if (painted) { unknown.push({ element: __name(el), selector: __path(el), text: text.slice(0, 40), why: painted }); continue; }
measured++;
const size = parseFloat(cs.fontSize);
const weight = parseInt(cs.fontWeight, 10) || 400;
const large = size >= 24 || (size >= 18.66 && weight >= 700);
// CSS opacity fades text against its own background. Ignoring it
// OVERSTATES contrast, which is the direction that hides a real failure.
const textAlpha = Math.min(1, fg.a * (parseFloat(cs.opacity) || 1));
const cr = ratio(textAlpha < 1 ? over({ ...fg, a: textAlpha }, bg) : fg, bg);
const need = large ? 3 : 4.5;
if (cr >= need) continue;Excludes tax, added at checkout where it applies.