Accessibility / WCAG 2.2 audit.
A 7,000-line in-browser audit bundle runs five test modules directly against the live DOM, returning per-criterion pass/fail mapped to WCAG 2.2 Level A, AA, and (in deep mode) AAA. Output is a graded W3C-style evaluation report; deep mode also emits a VPAT 2.4 export for procurement.
What it does accessibility.js
After the page renders, we inject accessibility-bundle.js via Playwright's page.evaluate(). It walks the DOM and runs five independent checkers:
- Structure — heading hierarchy, landmark roles, nesting validity, document outline
- ARIA — labels, roles, required attributes, abandoned
aria-*attributes - Forms — input labelling, fieldset legends, error-message association, required-field announcements
- Keyboard — tab order, focus visibility, focus traps, keyboard reachability of all interactive elements
- Contrast — computed foreground/background ratios per WCAG 1.4.3 (AA, 4.5:1) and 1.4.6 (AAA, 7:1)
Each finding lists the failing CSS selector, the WCAG criterion violated (e.g. 1.1.1 Non-text Content), the level (A / AA / AAA), severity, and a suggested fix. Aggregated counts produce a star rating per level — ★★★★★ when everything passes, dropping one star per ~20% of criteria failing. The overall rating is the worst of the three levels.
Two intensity modes:
- Basic (+0.05 credits/page, on by default) — Level A + AA criteria. Output: W3C-style WCAG Evaluation Report.
- Deep (+0.40 credits/page) — adds Level AAA, mobile touch-target (44×44 CSS px), and iOS VoiceOver heuristics. Output: same WCAG report plus a VPAT 2.4 export suitable for procurement / 508 compliance attestation.
What it finds
Real examples, mapped to WCAG criteria:
- Contrast (1.4.3 AA / 1.4.6 AAA):
.btn-primaryhas 3.8:1 contrast (white on #6FCB7B) — fails AA's 4.5:1 - Non-text content (1.1.1 A): 14
<img>tags withoutaltoraria-label— screen readers announce them as the filename - Labels or instructions (3.3.2 A): Login form's email input has no
<label for="…">and noaria-label - Info and relationships (1.3.1 A): Page jumps from
<h1>straight to<h3>twice — assistive tech may misrepresent document structure - Focus visible (2.4.7 AA): All
<a>tags haveoutline: noneand no replacement focus style — keyboard users can't see where they are - Keyboard trap (2.1.2 A): Cookie-consent modal traps focus on the "Accept" button — Escape doesn't close it, no programmatic dismissal
- Headings and labels (2.4.6 AA): Three pages share the exact title
"Untitled"— discoverable navigation is broken - Target size (2.5.5 AAA, deep only): Mobile bottom nav has 32×32 px tap targets — below the 44×44 recommendation
- Status messages (4.1.3 AA): Add-to-cart confirmation isn't announced via
role="status"— screen-reader users get no feedback - iOS VoiceOver heuristics (deep only): A custom dropdown built from
<div>s with click handlers — opens visually but is invisible to VoiceOver rotor
Coverage
- No real screen-reader playback (NVDA / JAWS / VoiceOver-on-Mac). Deep mode includes heuristics for iOS VoiceOver but doesn't substitute for a manual screen-reader pass on critical flows.
- No automated keyboard traversal of complex widgets (combobox, tree, tabs) — those patterns need contextual review.
- No color-blindness simulation — contrast ratios cover the most common deficiencies, not all eight types.
- No readability scoring (Flesch-Kincaid, comprehension grade-level) — those live in Persona Feedback as qualitative observations.
- WCAG conformance requires manual review of testable criteria the tool flags as "needs review" — the audit is an evidence pack, not a legal sign-off.
Sample finding
// One entry from report.accessibility.issues.AA[] { "criterion": "2.1.2 No Keyboard Trap", "level": "A", "severity": "high", "selector": "#consent-modal button.accept-all", "detail": "Tab cycles between the two consent buttons; Escape key does not\nclose the modal; no programmatic dismissal hook. Keyboard-only\nvisitors must reload the page to leave the consent dialog.", "fix": "Add a Cancel/Decline button focusable in the tab cycle, OR wire\nthe Escape key (keydown listener on the overlay) to programmatic close,\nOR add `role=dialog aria-modal=true` with an `aria-labelledby` heading\nand a close affordance.", "evidence": ["DOM screenshot: consent-modal-trap.png"] }