← Back to projects

Clear11y

Containerized accessibility scanner for pre-deployment static site testing.

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:

  1. Zip dist/ (or equivalent).
  2. Run Clear11y in a pinned Playwright container.
  3. Extract safely.
  4. Serve the extracted files via a temporary local HTTP server.
  5. 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 --> Build

Evidence (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.