a11yhawk
v0.2.0
Published
Open-source, self-hostable web accessibility scan engine: Playwright capture, Lighthouse audits, and BYOK LLM analysis producing structured WCAG reports.
Maintainers
Readme
A11yHawk
Open-source, self-hostable web accessibility scan engine. A11yHawk loads a page in a real browser, runs deterministic Lighthouse accessibility audits, and (optionally) has an LLM of your choice analyze the full page context - screenshot, accessibility tree, and sanitized HTML - against every WCAG criterion for the version and level you select. The result is a structured JSON report, a human-readable markdown report, a self-contained HTML report page, and an annotated screenshot with issues boxed on the page.
Runs entirely on your own infrastructure. No accounts, no telemetry, no phone-home; the only network traffic is the page you scan and the LLM endpoint you configure (and none at all in Lighthouse-only mode beyond the page itself).
Status: pre-1.0. The library API, CLI, and HTTP server mode are functional and tested. The API may change between minor versions until 1.0; see the CHANGELOG.
How it works
validate URL
|
v
Playwright capture
(screenshot, a11y tree, HTML)
|
v
Lighthouse audit
(deterministic; accessibility + optional performance)
|
v
LLM analysis
(BYOK, optional; skipped in Lighthouse-only mode)
|
v
parse and score
(always recomputed by the engine)
|
v
reports
(JSON, markdown, HTML, annotated screenshot)- Playwright capture: full-page screenshot (tiled to your LLM provider's image limits), Chrome DevTools accessibility tree, and HTML sanitized down to its accessibility-relevant structure.
- Lighthouse: accessibility-category audits in a subprocess, reusing the same browser over CDP. Findings are mapped to WCAG criteria and fed into the LLM prompt for cross-referencing. Opt in to the performance category and both run from the same page load (see Lighthouse performance category).
- LLM analysis (optional): the model receives the screenshot tiles, compact accessibility tree, sanitized HTML, Lighthouse findings, and a condensed matrix of every WCAG criterion for your chosen version + level, and returns structured issues with remediation guidance. Scores and statistics are recomputed by the engine from the actual findings, never trusted from the model.
- Lighthouse-only mode: omit the LLM config and you get a deterministic, no-API-key scan in a few seconds.
Requirements
- Node.js >= 20 (ESM-only package; use
import) - Chromium for Playwright (one-time):
npx playwright install chromiumfrom a project with a11yhawk installed, ornpx [email protected] install chromiumfrom anywhere else (browser downloads are pinned per Playwright version, and an unpinnednpx playwrightoutside a project resolves the registry's latest) - For LLM mode: an API key for OpenRouter or any OpenAI-compatible endpoint. You bring your own key and pay your own token costs; A11yHawk adds nothing on top.
Install
npm install a11yhawk
npx playwright install chromiumQuick start
Lighthouse-only (no API key needed):
import { scan } from 'a11yhawk';
const report = await scan('https://example.com');
console.log(report.structured.overallScore, report.structured.issues.length);Full AI analysis:
import { scan } from 'a11yhawk';
const report = await scan('https://example.com', {
llm: { apiKey: process.env.OPENROUTER_API_KEY },
wcagVersion: '2.2',
wcagLevel: 'AA',
onProgress: (e) => console.error(`[${e.stage}] ${e.message}`),
});Write the reports:
import { writeFileSync } from 'node:fs';
import { renderHtmlReport, scan } from 'a11yhawk';
const report = await scan('https://example.com', { llm: { apiKey: process.env.OPENROUTER_API_KEY } });
writeFileSync('report.json', JSON.stringify(report.structured, null, 2));
writeFileSync('report.md', report.markdown);
writeFileSync('report.html', renderHtmlReport(report)); // self-contained, opens from file://
if (report.annotatedScreenshot) writeFileSync('annotated.jpg', report.annotatedScreenshot);Scanning many pages? Hold an engine so the browser stays warm across scans:
import { A11yHawkEngine } from 'a11yhawk';
const engine = new A11yHawkEngine();
try {
for (const url of urls) {
const report = await engine.scan(url, { llm: { apiKey } });
// persist report however you like
}
} finally {
await engine.close();
}API
scan(url, options?) / engine.scan(url, options?)
One-shot scan() accepts ScanOptions & EngineOptions and manages the browser lifecycle for you. A11yHawkEngine takes EngineOptions in its constructor and ScanOptions per scan; it also exposes setConcurrency(n) (max concurrent page analyses, 1-10) and close() (shuts the browser down; the engine relaunches it on the next scan).
ScanOptions
| Option | Type | Default | Notes |
| ----------------------- | ---------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
| llm | ScanLlmOptions | omitted | Omit entirely for Lighthouse-only mode |
| llm.apiKey | string | required in LLM mode | Key for the configured endpoint |
| llm.model | string | anthropic/claude-sonnet-5 | Any model id your endpoint accepts |
| llm.baseUrl | string | https://openrouter.ai/api/v1 | Any OpenAI-compatible endpoint |
| llm.generationParams | GenerationParams | engine defaults | temperature, topP, frequencyPenalty, maxTokens |
| llm.debug | boolean | false | Verbose prompt/response logging |
| wcagVersion | '2.0' \| '2.1' \| '2.2' | '2.1' | |
| wcagLevel | 'A' \| 'AA' \| 'AAA' | 'AA' | |
| headers | ScanHeader[] | none | Custom request headers (cookies, auth) sent to the page |
| lighthouse | boolean \| ScanLighthouseOptions | true | false skips the audit (LLM mode only); an object selects categories, see below |
| lighthouse.categories | LighthouseCategory[] | ['accessibility'] | 'accessibility' \| 'performance'; must include 'accessibility'; one run, one page load |
| lighthouse.includeRaw | boolean | false | Attach the trimmed raw Lighthouse result as report.lighthouse.raw |
| screenshot | boolean | true | false skips screenshot capture entirely (Lighthouse-only mode; see Bulk scanning) |
| annotate | boolean | true | Draw severity-colored boxes on a copy of the screenshot |
| onProgress | (e: ScanProgressEvent) => void | none | Stages: validating, capturing, auditing, analyzing, processing, annotating, complete, failed |
| logger | Logger | console logger | Bring your own structured logger |
EngineOptions
| Option | Type | Default | Notes |
| ------------------------ | --------- | -------------- | ------------------------------------------------------- |
| allowPrivateNetworks | boolean | false | See Security before enabling |
| browser.disableSandbox | boolean | false | Chromium --no-sandbox, needed on some container hosts |
| browser.debug | boolean | false | Extra capture diagnostics |
| logger | Logger | console logger | |
ScanReport
| Field | Type | Notes |
| --------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| structured | StructuredScanOutput | Machine-readable source of truth: score, statistics, WCAG coverage, issues with remediation, passed checks |
| markdown | string | Human-readable report |
| screenshot | Buffer \| null | Full-page JPEG |
| annotatedScreenshot | Buffer \| null | Issues boxed on the page, when annotation resolved any selectors |
| lighthouse | LighthouseTransformedResult \| null | Lighthouse findings mapped to WCAG, plus lighthouseVersion and (when requested) performance / raw |
| usage | ScanUsage \| null | Tokens + cost in USD; null in Lighthouse-only mode |
| finalUrl | string | Guard-validated post-redirect URL that was actually analyzed |
| durationMs | number | |
Use renderHtmlReport(report) to turn any ScanReport into a single self-contained HTML document (inline CSS/JS, images as data URIs, renders from file://, itself WCAG AA accessible).
Lighthouse performance category
Add 'performance' to lighthouse.categories to collect performance metrics from the same Lighthouse run and page load as the accessibility audit; no second pass, no extra navigation. Works in Lighthouse-only mode (no API key) and in LLM mode alike:
const engine = new A11yHawkEngine();
const report = await engine.scan(url, {
lighthouse: { categories: ['accessibility', 'performance'] },
screenshot: false, // optional: skip capture in bulk Lighthouse-only scans
});
report.lighthouse.performance.score; // 0-100 (or null if uncomputable)
report.lighthouse.performance.metrics; // FCP/LCP/TBT/Speed Index (ms), CLS
report.lighthouse.performance.opportunities; // top improvements, largest first
report.lighthouse.lighthouseVersion; // provenance, e.g. "13.4.0"report.lighthouse.performance is a LighthousePerformanceResult:
| Field | Type | Notes |
| ---------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| score | number \| null | Category score 0-100; null when Lighthouse could not compute one (metrics may remain) |
| metrics.firstContentfulPaintMs | number? | FCP, milliseconds |
| metrics.largestContentfulPaintMs | number? | LCP, milliseconds |
| metrics.cumulativeLayoutShift | number? | CLS, unitless, 3 decimals |
| metrics.totalBlockingTimeMs | number? | TBT, milliseconds |
| metrics.speedIndexMs | number? | Speed Index, milliseconds |
| opportunities[] | array | Failing audits with an estimate: auditId, title, estimatedSavingsMs?, estimatedSavingsBytes?; sorted by time savings, capped at 10 |
Notes:
'accessibility'is always required incategories: it is the analysis source for the structured report. Omitting it throwsScanErrorinvalid-options.- A failed run surfaces through the existing
lighthouse-failedcode (fatal in Lighthouse-only mode, non-blocking in LLM mode); requesting performance adds no new error shapes. - Performance runs are serialized engine-wide, regardless of
setConcurrency(n): parallel traces on one machine contend for CPU and skew FCP/LCP/TBT. Other scan stages still run concurrently, so for the strictest metric fidelity run performance scans at concurrency 1 on an otherwise idle machine. - Performance runs get a 60s Lighthouse timeout (accessibility-only runs keep 30s).
lighthouse: { includeRaw: true }attaches the raw Lighthouse result asreport.lighthouse.rawfor audits the engine does not map, trimmed of its largest dead weight (full-page screenshot artifact, screenshot audits, localization tables).- The structured report (
report.structured) stays accessibility-only; performance data lives onreport.lighthouseand is not persisted intoStructuredScanOutput.
Bulk scanning
For crawling many pages, hold one A11yHawkEngine for the whole run instead of calling the one-shot scan() per page, and disable outputs you will not use:
const engine = new A11yHawkEngine();
try {
for (const url of urls) {
const report = await engine.scan(url, {
screenshot: false, // no capture, no tiling, no annotation, no Buffers in the report
lighthouse: { categories: ['accessibility', 'performance'] },
});
await persist(report);
}
} finally {
await engine.close();
}screenshot: falseskips screenshot capture entirely;report.screenshotandreport.annotatedScreenshotarenulland memory stays flat across thousands of sequential scans. It requires Lighthouse-only mode (the LLM analysis needs the screenshot), so combining it withllmthrowsinvalid-options.- Chromium shuts down whenever no scan is in flight and relaunches on the next one, returning its native memory to the OS between sequential scans by design. Overlapping scans (
setConcurrency(n)> 1 with concurrent callers) share one warm browser. - Every scan-time failure is a
ScanErrorwith aretryableflag; mapretryable: trueonto your crawler's retry queue and drop the rest. - The repo carries an opt-in soak test asserting flat memory and stable handle counts across sequential scans on one engine:
A11YHAWK_SOAK=1 A11YHAWK_SOAK_SCANS=1000 NODE_OPTIONS=--expose-gc npx vitest run src/engine/scan.soak.test.ts.
Error handling
Every pipeline failure throws a ScanError with a code and a retryable flag, so queue-based hosts can map failures onto their retry semantics:
| Code | Retryable | Meaning |
| ------------------- | --------- | -------------------------------------------------------------------- |
| invalid-options | no | Contradictory configuration (e.g. no LLM and Lighthouse disabled) |
| invalid-url | no | URL failed validation (malformed, non-http(s), or blocked target) |
| blocked-request | no | The SSRF guard blocked a navigation or redirect |
| capture-failed | yes | Browser navigation/capture failure (often transient) |
| lighthouse-failed | yes | Lighthouse failed in Lighthouse-only mode (non-blocking in LLM mode) |
| llm-auth | no | API key invalid or expired |
| llm-rate-limit | no | Endpoint rate limit hit |
| llm-failed | yes | Other LLM call failure |
| llm-malformed | no | Model response was not parseable as the expected JSON |
import { ScanError, scan } from 'a11yhawk';
try {
await scan(url, options);
} catch (error) {
if (error instanceof ScanError && error.retryable) requeue(url);
else reportPermanentFailure(error);
}CLI
Run it without installing anything:
# Free Lighthouse-only scan; reports written to ./a11yhawk-output/<timestamp>/
npx a11yhawk https://example.com
# Full AI analysis (bring your own OpenRouter-compatible key)
npx a11yhawk https://example.com --api-key sk-...
# One-time Chromium download if you have never used Playwright on this machine.
# The version pin matters here: outside a project, an unpinned `npx playwright`
# resolves the registry's latest, whose browser builds a11yhawk cannot use.
# `npx a11yhawk doctor` prints the exact command when Chromium is missing.
npx [email protected] install chromiumThe default command scans a single URL and writes report.json and report.md (add html for the self-contained HTML report), plus screenshot.jpg and annotated.jpg when available. LLM mode turns on automatically when an API key is present (flag or env) and --no-llm is absent; otherwise the scan runs Lighthouse-only, which needs no key and finishes in seconds.
Commands
| Command | Description |
| -------------------------- | ----------------------------------------------------- |
| a11yhawk <url> [options] | Scan a URL (default command) |
| a11yhawk serve [options] | Run the HTTP server (see Server mode) |
| a11yhawk doctor | Verify Chromium, Lighthouse, and key configuration |
| a11yhawk --help | Show usage |
| a11yhawk --version | Print the version |
Scan options
| Flag | Default | Description |
| ------------------------ | ------------------------------- | ------------------------------------------------------------------------ |
| --model <id> | anthropic/claude-sonnet-5 | LLM model id (env: A11YHAWK_MODEL) |
| --api-key <key> | none | LLM API key; presence enables LLM mode (env: A11YHAWK_API_KEY) |
| --base-url <url> | https://openrouter.ai/api/v1 | OpenAI-compatible endpoint (env: A11YHAWK_BASE_URL) |
| --no-llm | off | Force Lighthouse-only mode even if a key is set |
| --wcag <2.0\|2.1\|2.2> | 2.1 | WCAG version |
| --level <A\|AA\|AAA> | AA | Conformance level |
| --header "Name: value" | none | Custom request header; repeatable |
| --format <list> | json,md | Comma-separated output formats: json, md, html |
| --output <dir> | ./a11yhawk-output/<timestamp> | Directory for report files and screenshots |
| --stdout | off | Print structured JSON to stdout instead of writing files |
| --open | off | Open report.html after writing (implies html in --format) |
| --fail-below <score> | none | Exit 1 when the overall score is below <score> (CI gate) |
| --allow-private | off | Permit scanning private/internal targets (env: A11YHAWK_ALLOW_PRIVATE) |
| --no-annotate | off | Skip screenshot annotation |
| --no-lighthouse | off | Skip the Lighthouse audit (LLM mode only) |
| --quiet | off | Errors only |
| --verbose | off | Debug logging |
Progress is streamed to stderr as [stage] message; report data goes to stdout only under --stdout, so a11yhawk <url> --stdout > report.json pipes cleanly. --stdout writes no files, so it takes precedence over --open.
Environment variables
The CLI reads these as fallbacks; an explicit flag always wins. The library itself never reads the environment.
| Variable | Equivalent flag | Notes |
| -------------------------- | ----------------- | --------------------------------------------------------------------------- |
| A11YHAWK_API_KEY | --api-key | Enables LLM mode when set |
| A11YHAWK_MODEL | --model | LLM model id |
| A11YHAWK_BASE_URL | --base-url | OpenAI-compatible endpoint |
| A11YHAWK_ALLOW_PRIVATE | --allow-private | Set to true to permit private/internal targets |
| A11YHAWK_DISABLE_SANDBOX | (none) | Set to true to launch Chromium with --no-sandbox (some container hosts) |
Exit codes
| Code | Meaning |
| ---- | ---------------------------------------------------------------------------- |
| 0 | Success |
| 1 | Scan succeeded but the score was below --fail-below |
| 2 | Scan error (a ScanError; its [code] message is printed to stderr) |
| 3 | Configuration error (bad flag, contradictory options, or a doctor blocker) |
doctor
a11yhawk doctor verifies that a scan can run at all: Node.js >= 20, a usable Playwright Chromium (it prints the version-pinned npx playwright@<version> install chromium command matching its bundled Playwright when that is what is missing), and a resolvable Lighthouse CLI. It also reports whether an API key is configured, which is informational only since Lighthouse-only mode needs none. It exits 0 when Lighthouse-only scanning is possible and 3 otherwise, so it doubles as a CI preflight.
CI example
# .github/workflows/a11y.yml
name: Accessibility
on: [push]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npx [email protected] install --with-deps chromium
- name: Accessibility gate
run: npx a11yhawk https://your-site.example --fail-below 90 --format json,html --output a11y-report
- if: always()
uses: actions/upload-artifact@v4
with:
name: a11y-report
path: a11y-report/Add --api-key ${{ secrets.OPENROUTER_API_KEY }} for full AI analysis. The job fails when the score drops below 90 and always uploads the report as a CI artifact.
Server mode
a11yhawk serve runs a small HTTP service over the same engine, with one warm browser shared across scans. Jobs live in memory: there is no database, and a restart drops every job and report. Persistence is the host's concern by design.
Running it
# From an install, or a clone after `npm run build`
a11yhawk serve # listens on :4000
# Docker (browser + system deps baked in; the recommended path).
# Images are published to GHCR with each tagged release.
docker run --rm -p 4000:4000 ghcr.io/charlesjones-dev/a11yhawk:latest
# Docker Compose (see examples/docker-compose.yml)
docker compose -f examples/docker-compose.yml upEndpoints
| Method and path | Description |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| POST /scans | Enqueue a scan. Body { "url": "...", "options": { ... } }. Returns 202 { id }. |
| GET /scans/:id | Job status, plus the report once completed or an error once failed. |
| GET /scans/:id/report.html | The rendered HTML report. 404 until the scan completes. |
| GET /healthz | Liveness, uptime, and job counts. Never requires auth. |
Enqueue a scan and poll it to completion:
# Lighthouse-only scan (no API key needed)
id=$(curl -s localhost:4000/scans \
-H 'content-type: application/json' \
-d '{"url":"https://example.com"}' | jq -r .id)
# Poll until .status is "completed" or "failed"
curl -s localhost:4000/scans/$id | jq '{status, score: .report.structured.overallScore}'
# Save the HTML report once it is ready
curl -s localhost:4000/scans/$id/report.html -o report.htmlThe options object accepts only llm { apiKey, model, baseUrl, generationParams }, wcagVersion, wcagLevel, headers, lighthouse (boolean or { categories, includeRaw }, same semantics as the library option), screenshot, and annotate; any other field is ignored. In the JSON report, screenshots come back as base64 data: URIs rather than raw bytes, and a submitted llm.apiKey is never echoed back in any response.
Configuration
Every setting is an environment variable, read only by the server layer (never the library):
| Variable | Default | Purpose |
| --------------------------- | ------- | ------------------------------------------------------------------------------------ |
| A11YHAWK_PORT (or PORT) | 4000 | Listen port. serve --port <n> overrides both. |
| A11YHAWK_AUTH_TOKEN | (unset) | When set, every endpoint except /healthz requires Authorization: Bearer <token>. |
| A11YHAWK_CONCURRENCY | 2 | Max concurrent scans (clamped 1-10). Extra scans queue FIFO. |
| A11YHAWK_JOB_TTL_SECONDS | 3600 | How long a finished job is retained before eviction. |
| A11YHAWK_ALLOW_PRIVATE | false | Permit scanning private/internal targets (see the note below). |
| A11YHAWK_DISABLE_SANDBOX | false | Launch Chromium with --no-sandbox; some container hosts require it. |
Server security note
allowPrivateNetworks is a server-level posture, set only by A11YHAWK_ALLOW_PRIVATE. A request body can never turn it on: that field, along with browser and logger, is stripped from submitted options. Leave it off anywhere scan URLs come from people you do not trust, and put network-layer egress rules around the server when it is multi-tenant. See the Security section below for the residual Lighthouse SSRF caveat that applies to every mode.
Security
A11yHawk is designed to be safe to embed in services where scan URLs come from untrusted users, and honest about its residual risks.
- SSRF request guard, default-on. Every request the scanned page makes is validated at the browser context level: scheme checks, per-request DNS resolution with no cached allow-verdicts, redirect-hop detection, service workers blocked, private/loopback/link-local targets refused. Scan URLs are re-validated at scan time (not just submission time) to narrow DNS-rebinding windows.
allowPrivateNetworks: trueexists because scanning your own internal apps is a primary self-hosting use case. It only widens which resolved addresses the guard accepts; every other protection stays active. Leave itfalseanywhere scan URLs come from people you don't trust.- Known residual risk: Lighthouse drives its own browser navigation, outside the Playwright request guard. The engine re-validates the audit target immediately before the run, but network-layer egress filtering is the only complete mitigation. If you run A11yHawk multi-tenant, put egress rules around it.
- Subprocess hygiene: the Lighthouse CLI is spawned with
shell: falseand argv arrays; the attacker-controllable URL is never interpreted by a shell. - Key handling: API keys arrive as options, are never logged, and error messages from the LLM layer are sanitized so keys cannot leak through error chains.
- Report output: everything interpolated into the HTML report is entity-escaped, so a malicious scanned page cannot inject markup or script into its own report.
- No telemetry. Nothing is collected, nothing phones home.
Found a vulnerability? Please open a GitHub security advisory rather than a public issue.
For AI agents
Everything above applies; this section is deliberately compact and exact.
- Package:
a11yhawk(npm). ESM only. Node >= 20. Requires Chromium:npx [email protected] install chromium(pin matches the bundled Playwright; unpinned is fine only inside a project that has a11yhawk installed). - Exports:
scan,A11yHawkEngine,renderHtmlReport,ScanError,createLogger,DEFAULT_MODEL, plus all types (ScanOptions,EngineOptions,ScanReport,StructuredScanOutput,AccessibilityIssue, ...). Full.d.tsshipped. - Minimal complete program:
import { scan } from 'a11yhawk';
const report = await scan('https://example.com'); // Lighthouse-only, no key needed
// report.structured.overallScore: number 0-100
// report.structured.issues: Array<{ severity: 'critical'|'high'|'medium'|'low', wcagCriteria: string, title: string, remediation: string, ... }>- LLM mode: add
{ llm: { apiKey } }; key is an OpenRouter key unlessllm.baseUrlpoints elsewhere. Expect 1-5+ minutes and provider token costs per scan in LLM mode; Lighthouse-only takes seconds and is free. - Scanning localhost or private hosts requires
{ allowPrivateNetworks: true }. - All failures are
ScanErrorwith.code(see table above) and.retryable. Non-retryable codes will fail identically on retry; do not loop on them. - CI gating pattern: run a scan, compare
report.structured.overallScoreto your threshold, exit non-zero below it. Or use the CLI's built-in gate:npx a11yhawk <url> --fail-below 80. - Shelling out instead of importing:
npx a11yhawk <url> --stdoutprints the structured JSON to stdout (progress goes to stderr). Exit codes: 0 success, 1 below--fail-below, 2 scan error, 3 configuration error.npx a11yhawk doctorpreflights the environment. - A self-hostable HTTP mode exists (
a11yhawk serve,POST /scansthen pollGET /scans/:id); see Server mode above.
Examples
Runnable scripts live in examples/. They import a11yhawk by package name (Node resolves the package's own name from inside the repo), so every script is byte-for-byte what you would write in your own project.
From a clone:
npm install
npx playwright install chromium # one-time browser download
npm run build # examples resolve 'a11yhawk' to the local dist/ buildThen:
# Self-contained demo: serves a deliberately broken page locally, scans it,
# writes an HTML report to examples/output/. No API key, no external network.
node examples/local-fixture.mjs
# Free Lighthouse-only scan of any public URL, summary to stdout
node examples/lighthouse-only.mjs https://your-site.example
# Full AI analysis: writes report.json / report.md / report.html + screenshots
# to examples/output/. Takes minutes and costs tokens on your key.
OPENROUTER_API_KEY=sk-... node examples/full-scan.mjs https://your-site.example
# CI gate: exit 0 if score >= threshold, 1 below it, 2 on scan error
node examples/ci-gate.mjs https://your-site.example 80full-scan.mjs also honors A11YHAWK_MODEL to override the default model. If you copy a script into your own project, replace the build step with npm install a11yhawk; nothing else changes.
The directory also holds two deployment templates: docker-compose.yml (server mode as a container) and github-action.yml (a copy-paste CI job gating on --fail-below).
Output formats
structured(JSON): overall score (0-100, recomputed from WCAG coverage), per-severity statistics, WCAG coverage with pass/fail per criterion, issues with location/selector, code context, impact, and remediation, passed checks. Stable shape; treat as the source of truth.markdown: the same content as a readable report.- HTML (via
renderHtmlReport): a single dark-theme file with score ring, severity breakdown, sortable/collapsible issues with client-side resolve tracking (localStorage), and the annotated screenshot. Attach it to CI artifacts, tickets, or email; it has zero external dependencies.
Roadmap
- GitHub Action wrapper.
- SARIF and JUnit output formats.
Contributing
Contributions welcome. Setup:
git clone https://github.com/charlesjones-dev/a11yhawk
cd a11yhawk
npm install
npx playwright install chromium
npm run verify # lint + typecheck + tests + build; must pass before a PR| Script | What it does |
| ------------------ | -------------------------------------------- |
| npm run verify | The full gate: lint, typecheck, tests, build |
| npm test | Vitest in watch mode |
| npm run test:run | Tests once |
| npm run format | Prettier write |
Conventions: TypeScript strict (including noUncheckedIndexedAccess), ESM only, tests colocated as *.test.ts next to the module they cover. The request guard, URL validator, and Lighthouse spawn are security-sensitive; changes there get extra scrutiny and must not weaken defaults. Please keep PRs focused and include tests for behavior changes.
Relationship to AccessHawk
A11yHawk is the open-source engine behind AccessHawk, extracted and maintained by the same author. The hosted product adds scheduling, history, dashboards, teams, and a managed API around this engine; the engine itself - capture, audits, AI analysis, and reports - is all here, and running it on your own infrastructure is the point.
License
Apache-2.0. Copyright Charles Jones.
