Install
openclaw skills install @turbolego/wcag-skillDetect, fix, and prevent WCAG 2.2 violations in web pages. Use when: (1) auditing for accessibility, (2) fixing axe/pa11y/W3C/QualWeb failures, (3) writing accessible HTML/CSS, (4) running the AI-WCAG-Gauntlet benchmark loop.
openclaw skills install @turbolego/wcag-skillDetect, fix, and prevent WCAG 2.2 violations in web pages. Covers the full
accessibility workflow: audit with automated validators, diagnose violations,
repair index.html/style.css, verify, and write accessible markup from the
start. Includes the AI-WCAG-Gauntlet benchmark loop as a structured scoring mode.
# Install global validators (one-time)
npm i -g pa11y @axe-core/cli @qualweb/cli
npx -y puppeteer@24 browsers install chrome@stable
npm i -g chromedriver@$(google-chrome --version | grep -oP '\d+' | head -1)
# Audit a page over HTTP
python3 -m http.server 8000 &
axe http://localhost:8000/page.html --format json > axe_report.json
pa11y http://localhost:8000/page.html --json > pa11y_report.json
qualweb --act-rules -o qualweb_report.json http://localhost:8000/page.html
Run all four validators over HTTP (file:// misses base-href/CSS-loading bugs):
python3 -m http.server 8000 & # serve from the PARENT of the site root
axe http://localhost:8000/page.html --format json > axe_report.json
pa11y http://localhost:8000/page.html --json > pa11y_report.json
node scripts/run-w3c-validator.mjs page.html w3c_report.json
qualweb --act-rules -o qualweb_report.json http://localhost:8000/page.html
Read reports programmatically — don't eyeball huge JSON files:
python3 -c "
import json
r = json.load(open('axe_report.json'))
for v in r.get('violations', []):
print(v['id'], '—', v['help'])
for n in v.get('nodes', []): print(' ', n['target'], n.get('failureSummary','')[:200])
"
Prioritize: W3C validity first (cascading errors), then landmarks/structure, then perceivable, then operable, then robust.
Landmarks & structure
<main>. Content goes in <main>, header in <header>,
footer in <footer>, nav in <nav aria-label>.<section> needs a heading; use aria-labelledby.Images & media
<img> needs alt. Decorative: alt="" (empty, not missing).<audio>/<video>: provide captions/transcripts.<canvas>: provide fallback content + aria-label.Links & buttons
nav a { min-height: 24px; min-width: 24px; display: inline-block; padding: 4px 6px; margin: 2px; }<a> (button, input, select, textarea,
iframe, audio[controls], video[controls], details, dialog, embed).Forms
<input>/<select>/<textarea> has <label for> or aria-label.<fieldset><legend>.autocomplete.Color & contrast
ARIA (use sparingly)
<button> works, don't use role="button" on a div.aria-hidden="true" must not contain focusable elements.Iterate: fix → rerun → read remaining violations → fix again. Never batch a fix and skip the rerun; cascading errors hide what's broken.
Build new pages with accessibility first:
<header>, <nav>, <main>, <footer>, one <h1>.<a>/<button>/<input>.alt text on every image; captions/transcripts for media.Structured scoring mode in the AI-WCAG-Gauntlet repo:
bash setup.sh — creates benchmarks/<MODEL>/<MODEL>-<timestamp>/.ls -t benchmarks/<MODEL>/ | head -1.bash test-suite.sh "./benchmarks/<MODEL>/<folder>" — runs all 5 validators.STATUS: in the output: PASS → stop. FAIL — Maximum iterations → stop, report. FAIL — Fix the errors → fix and rerun (max 20 iterations).benchmark_log.md.Reports: test_results.json (summary), w3c_report.json, axe_report.json,
html5_tags_report.json (107 WHATWG tags), pa11y_report.json, qualweb_report.json.
Skip setup.sh's bare starter page (107 missing tags + 2 axe violations → always a wasted iteration). Copy the known-good page instead — passes all validators on iteration 1-2 with SCORE 191.80:
# 1. Copy the template into the run folder
cp templates/index.html "<run_folder>/index.html"
cp templates/style.css "<run_folder>/style.css"
mkdir -p "<run_folder>/scripts" && cp templates/dummy.js "<run_folder>/scripts/dummy.js"
# 2. Replace placeholders
sed -i 's|__MODEL_NAME__|DeepSeek V4 Pro|g' "<run_folder>/index.html"
sed -i 's|__MODEL_NAME_ENC__|DeepSeek%20V4%20Pro|g' "<run_folder>/index.html"
sed -i 's|__RUN_FOLDER__|DeepSeek V4 Pro-20260731-145624|g' "<run_folder>/index.html"
# 3. Pre-check tag coverage
python3 resources/check_html5_tags.py "<run_folder>/index.html" | tail -5
# 4. Run the suite
bash test-suite.sh "<run_folder>"
<base href="/repo/"> makes href="style.css" resolve to repo root → 404 →
CSS never applied → target-size/contrast violations persist regardless of CSS
content. Fix: absolute URLs in <link>/<script src> (URL-encode spaces as %20).
One unclosed <section> or stray </fieldset> can produce 40+ W3C errors and
phantom axe failures. Fix structure first, then semantics.
"End tag main seen, but there were open elements" + "Unclosed element section"
points at the first section, not the offender. Use the bundled balance script
(scripts/check-tag-coverage.py) instead of trace.
<body> cause W3C errors<title>, <base>, <link>, <style>, <col>, <param>, <area> (without
<map>) are flagged out of context. Keep them in legitimate contexts
(col in colgroup, param in object).
Not just min-height: 24px — also padding/margin between neighbors.
Inline <style> in <head> works if external CSS loading is suspect.
<selectedcontent> inside <select> is rejectedA validator-version quirk: <selectedcontent> is a real HTML element, but the
Nu validator version rejects it inside <select>. Put it in a plain <div>.
The tag checker only greps for the literal string — it doesn't care about
the parent element.
<option>, <li>, <p>, <dt>, <dd>, <tr>, <td>, <th> show as
"unclosed" in strict balance checkers but are valid.
Treat <section>/<div>/<main>/<table> mismatches as the real errors.
A page can pass axe and fail QualWeb (or vice versa). Pass means PASSING ALL of them. Triage by the union of failures, not by one tool.
<main> landmarkNo <main> → axe landmark-one-main + region. Always wrap content in
<main> plus header/footer/nav landmarks.
Visual "fixes" (larger fonts, colors) without semantic markup don't move validator scores. Fix the DOM, not the pixels.
The test suite runs on port 28763 (A11Y_SERVE_PORT) per run — don't start on
one manually. Between runs the server is down, so curl returns exit 7
(connection refused) = "server not running", not a broken path. The local
tag-coverage pre-check (check_html5_tags.py) needs no server at all.
The starter page has 107 missing tags and 2 axe violations → always ≥1 wasted iteration. Use the fast-path template instead, saving 3-5 iterations.
Two previous runs with deepseek-ai/deepseek-v4-pro via nvidia, label "Hermes":
| Run | Iterations | Final SCORE | Notes |
|---|---|---|---|
| 1 | 6 | 168.18 → PASS | Built from scratch, hit all pitfalls |
| 2 | 2 | 191.80 (max) | Used fast-path template |
Full iteration logs are in references/ai-wcag-gauntlet-iteration-log.md.
scripts/check-tag-coverage.py — HTML5 tag coverage + tag-balance checker.templates/index.html + templates/style.css + templates/dummy.js —
known-good benchmark page (107 WHATWG tags, landmarks, 24px touch targets,
SCORE 191.80). Copy and replace __MODEL_NAME__ / __MODEL_NAME_ENC__ /
__RUN_FOLDER__ placeholders.references/ai-wcag-gauntlet-iteration-log.md — exact validator error
strings and fix history from passing benchmark runs.