Clear11y is a pre-deployment accessibility scanner designed for static site builds. Instead of “deploy, then test”, it treats your dist/ artifact as the input, scans it inside a containerized Playwright runtime, and outputs evidence-rich WCAG reports you can gate in CI.
At a glance (from the codebase)
| Metric | What it looks like |
|---|---|
| Scale | ~20.8K LOC (Python + templates + schemas) |
| Core package | src/scanner/ (pipeline.py, services/*, reporting/*) |
| Interfaces | CLI (src/scanner/cli.py), FastAPI server (src/scanner/web/server.py), GitHub Action (action.yml) |
| Engines | axe-core via axe-playwright-python, plus a custom keyboard engine |
| Security posture | Zip Slip + zip bomb guards, SSRF mitigation for live URL scans |
Measured numbers (2026-01-16)
- Package version: 0.4.1 (
pyproject.toml). - Python requirement: >= 3.10.
cloc(core surface:src/,tests/,action.yml,README.md,pyproject.toml): 9,709 LOC across 50 files.cloc(full repo, excluding.git,node_modules,dist,build,.venv): 20,856 LOC across 85 files.- Default scan concurrency: 4 pages (
src/scanner/core/settings.py,DEFAULT_CONFIG.max_concurrency). - Report templates: 6 Jinja templates, 2 JSON schemas.
- Python test files: 3 (
tests/**/*.py).
Problem
Static site generators produce final HTML in dist/ or build/, but the most common a11y workflow (browser extensions) breaks down because file:// scanning is restricted and inconsistent.
Teams get forced into bad tradeoffs: deploy first and test in production, stand up local servers that don’t match real artifacts, or skip automated testing entirely. That’s how accessibility regressions slip into releases.
Constraints
- Must scan the exact build artifact, not a staging deployment.
- Results must be consistent across local runs and CI.
- Uploaded ZIPs and scanned URLs are untrusted input (Zip Slip/zip bombs/SSRF).
- Multi-page sites need parallelism, but focus-stateful checks must stay correct.
Solution
Treat the build artifact as the input:
- Zip
dist/(or equivalent). - Run Clear11y in a pinned Playwright container.
- Extract safely.
- Serve the extracted files via a temporary local HTTP server.
- Run axe + keyboard tests, capture screenshots, and build an HTML report plus structured JSON.
That enables a simple gate:
Build → Scan → Deploy
If violations exceed a threshold, the pipeline fails, and you fix issues before anything ships.
Architecture
flowchart TB
subgraph Inputs["Interfaces"]
CLI["CLI"]
API["FastAPI server"]
GH["GitHub Action"]
end
CLI --> PIPE["Pipeline"]
API --> PIPE
GH --> PIPE
PIPE --> DOCKER["Docker runtime (pinned Playwright)"]
DOCKER --> ZIP["ZipService (Zip Slip + size guard)"]
DOCKER --> HTTP["HttpService (ephemeral localhost server)"]
DOCKER --> PW["BrowserManager (Playwright lifecycle)"]
PW --> AXE["PlaywrightAxeService (axe-core)"]
PW --> KB["KeyboardAccessibilityService"]
PIPE --> DB[(SQLite / PostgreSQL)]
PIPE --> REPORT["HTML report + JSON + screenshots"]flowchart LR
Build[Build static site] --> Zip[Zip dist/]
Zip --> Scan[Clear11y scan]
Scan --> Report[Report + evidence]
Report --> Gate{Thresholds met?}
Gate -- Yes --> Deploy[Deploy]
Gate -- No --> Fix[Fix issues]
Fix --> BuildEvidence (placeholders)
- Screenshot (TODO):
case-studies/clear11y/html-report-summary.png- Capture: top of the generated HTML report showing totals by impact and affected pages.
- Alt text: “Accessibility report summary showing violations by severity and page.”
- Why it matters: proves the output is human-readable and actionable.
- Screenshot (TODO):
case-studies/clear11y/violation-screenshot-evidence.png- Capture: one violation detail view with the highlighted element screenshot.
- Alt text: “Violation detail with screenshot evidence highlighting the failing element.”
- Why it matters: supports the claim that reports include evidence, not just rule IDs.
- Screenshot (TODO):
case-studies/clear11y/github-action-gate.png- Capture: a GitHub Actions run showing the scan step failing a build on threshold.
- Alt text: “GitHub Actions job failing due to accessibility threshold violations.”
- Why it matters: supports the “Build → Scan → Deploy” CI gating workflow.
- Screenshot (TODO):
case-studies/clear11y/json-report-schema.png- Capture: a snippet of the consolidated JSON report next to its schema validation step/log.
- Alt text: “Consolidated JSON report with schema validation output.”
- Why it matters: supports the claim that outputs are machine-readable and validated.
Deep dive: the “why it’s reliable” parts
1) It does not pretend file:// is stable
Inside the container, Clear11y serves the extracted artifact via HttpService (src/scanner/services/http_service.py) and scans http://localhost:<port>/.... This sidesteps browser restrictions while keeping the scan pinned to the exact build output.
2) Concurrency where it’s safe, sequential where it’s correct
The architecture explicitly separates:
- Stateless scans (axe): safe to parallelize.
- Stateful scans (keyboard): focus-stateful, so it runs sequentially to stay correct.
3) Evidence-first reporting
Reports are generated from a consolidated JSON model:
- HTML report builder:
src/scanner/reporting/jinja_report.py - JSON schema validation:
src/scanner/schemas/validator.py - Artifact packaging:
src/scanner/reporting/artifacts.py
This makes CI gating and downstream tooling much easier than “parse a blob of HTML”.
4) Hardened inputs (because ZIPs and URLs are untrusted)
Clear11y has explicit guardrails in the implementation:
- Zip Slip protection + uncompressed-size limit:
src/scanner/services/zip_service.py - SSRF mitigation for live URL scans (block non-public destinations):
src/scanner/web/server.py
Tech stack
| Area | Choices |
|---|---|
| Core | Python 3.10+, FastAPI, Playwright, axe-core |
| Reports | HTML report generation (templating + JS UX) |
| Storage | SQLite for local, PostgreSQL for durable job history |
| Deployment | Docker container, GitHub Actions integration |
Key decisions
- Docker-first runtime: pin browsers and dependencies for repeatable results.
- Artifact-first scanning: ZIP input keeps the scan anchored to what you will deploy.
- Dual engines: axe for rules, plus keyboard testing for focus/navigation behavior.
- Schema-validated outputs: consolidated JSON is validated to stay stable for automation.
Tradeoffs
- Containerized scanning adds overhead compared to “just run a browser extension”.
- ZIP uploads require careful input limits and extraction hardening.
- Keyboard checks are slower because they are stateful and run sequentially.
Security and reliability
- ZIP extraction is hardened (path traversal checks + uncompressed size guard).
- Live URL scanning blocks non-public destinations to reduce SSRF risk.
- Results are generated from consolidated JSON so report generation failures can be isolated.
Testing and quality
- Unit and integration tests live under
tests/(including container scan integration coverage).
Outcomes
- You can gate deployments on a single deterministic scan of the real build artifact.
- Reports include screenshots and structured data, making a11y work actionable instead of “mysterious failures”.
- The same scan semantics run locally, in CI, and in a self-hosted API mode.