This is a real deliverable CivicBinder produces — a filed ADA Title II readiness binder for a small municipality. It's live: point it at a city's web presence, and it audits, gathers evidence, and assembles the whole thing. Everything below is how it actually works.
The April 2024 DOJ rule gave state and local governments a hard deadline to conform their web content to WCAG 2.1 AA. A small city clerk cannot read a WCAG spec. So the interesting engineering problem isn't "run a scanner" — it's turning scanner output into something that survives a records request.
Watch the CivicBinder — ADA Title II compliance, done for you demo
Automated scans cover maybe a third of WCAG
The first thing you learn: axe-core (and every tool built on it) is honest about only catching ~30–40% of WCAG success criteria. Color contrast, missing alt text, form labels, ARIA misuse — machine-verifiable. But "is this alt text meaningful?" or "does the reading order make sense?" — not.
If you dump raw axe violations into a PDF and call it a binder, you've produced a document that quietly claims full coverage while silently omitting the half a scanner can't see. That's the opposite of defensible. So the core design decision is a three-way split on every success criterion:
for each SC in WCAG_2_1_AA:
checks = rules_mapping[SC] # axe rule ids → SC
if checks and all machine-verifiable:
status = run_axe(checks) # PASS / FAIL, with evidence
elif checks partially cover SC:
status = "PARTIAL — machine-checked + needs review"
else:
status = "MANUAL — human attestation required"
Every SC lands in exactly one bucket, and the binder shows all three. The clerk sees precisely what the tool verified and what a human still has to sign off on. Honesty is the feature.
The mapping table is the actual product
axe emits rule ids like color-contrast and image-alt. WCAG is organized by success criterion — 1.4.3, 1.1.1. The glue is a hand-curated map, because it's many-to-many: one SC can need several axe rules, and one rule can touch several SCs.
"1.1.1": ["image-alt", "input-image-alt", "area-alt", "object-alt"],
"1.4.3": ["color-contrast"],
"4.1.2": ["aria-required-attr", "button-name", "link-name", ...],
Get this table wrong and the whole binder is wrong. It's the part I'd never let a model hallucinate — it's reviewed against the official ACT rules and pinned to an axe version, because rule ids drift between releases.
Evidence needs provenance, not just a boolean
A "PASS" with no proof is worthless in a filing. So for every finding the crawler captures, at scan time:
- the URL and a content hash of the page,
- the DOM node and selector that triggered (or cleared) the rule,
- a screenshot with the element highlighted,
- an ISO-8601 timestamp.
That bundle is what makes a line in the table a piece of evidence instead of an assertion. When someone asks "prove the city was compliant on this date," the binder answers with a timestamped artifact, not a claim.
The crawl is messier than a SaaS site
Municipal web presence is not one Next.js app. It's a main .gov site, a third-party permit portal, a legacy PDF library, an agenda-management subdomain, and an embedded payment iframe — different owners, different auth. The crawler treats each surface as its own scope with its own coverage report, and PDFs get routed to a separate document-accessibility check entirely (tagged structure, reading order), because axe only speaks HTML.
The lesson that generalizes past ADA: compliance tooling lives or dies on the boring mapping layer and the evidence provenance — not the scanner. The scanner is a commodity; the defensible trail around it is the work.
I built this into a done-for-you product — book a slot and I'll run your city's binder.

Top comments (0)