Skip to main content

Automated accessibility testing for pull requests

For developers who hate compliance tooling. Your PR ships a missing alt attribute: the comment tells you. One click copies the unified diff. You merge. No screenshots. No 200-row spreadsheets.

.github/workflows/accessibility.yml
name: Accessibility
on: pull_request

permissions:
  pull-requests: write   # post the results comment
  id-token: write        # count free-tier quota against this repo

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: accessibility-pro/action@v2
        with:
          url: ${{ secrets.STAGING_URL }}
          wcag-level: AA
          fail-on: error

Free tier, no account

10 scans per day per repository without a token, all five engines plus the live-DOM verifier. Enough to dogfood the workflow before committing to one of the paid plans with monthly CI scan allowances

Verified patches only

Every Copy-as-PR diff is sandbox-validated. If the patch would regress a non-targeted rule we hand you a snippet instead. What the loop covers today

The scanner owns the verdict

fail-on: error blocks merges on critical and high findings; fail-on: wcag blocks on any Level A or AA failure. The build result and the hosted report can never disagree. One sticky comment per PR, SARIF for the Security tab.

What the fix loop covers today

A finding becomes a Copy-as-PR diff only when its fix class is switched on, the patch applies to the captured page, and the re-scan clears at least 80% of the targeted occurrences with no new violation. Everything else arrives as a snippet with notes for a person.

Fix classes the sandbox loop handles, and their status
Fix classWCAGStatus
Missing or empty alt text1.1.1On: diff when verified
Unnamed buttons, links and controls (aria-label)4.1.2On: diff when verified
Form fields without a label1.3.1, 3.3.2On: diff when verified
Heading order, link text, colour contrast, aria-describedby references1.3.1, 2.4.4, 1.4.3, 4.1.2Built, switched off until their fixes pass the sandbox re-scan: snippet with notes
Everything elseanySnippet with notes, never a diff
  • Patches are applied to the HTML captured for the first page of a scan; a finding on another page gets a snippet.
  • Up to five patch generations per scan per day, on the Developer plan and above.
  • The re-scan runs axe-core on the patched copy; a diff that clears the target but fails the re-scan is never shown.

Find where a defect comes from

  • Shared templates. A finding seen on at least half of the scanned pages is listed under Site-wide / template findings, with the number of pages it appeared on. It most likely comes from a shared header, footer or widget, so the template is the place to fix it.
  • WordPress plugins and themes. A finding rendered by a plugin or theme carries its name and version, for example Plugin · wpforms-lite 1.8, so you know whose code to change or which version to quote upstream. The report lists the theme and plugins it found, and you choose whether plugin markup counts toward the score.

Accuracy is measured on hand-annotated WCAG fixtures, including false-positive traps built from real-world noise: read the published precision and recall. The same scanner on real websites, before and after a round of fixes, is in the field audit of live sites.Every named rule it can report is listed in the rule catalogue, with where each one comes from, where it runs, and whether we have seen it fire.

How scans handle cache & freshness

  • Outbound cache-bust. Every page fetch appends ?_apscan=<scan_id> and sends Cache-Control: no-cache. Cloudflare, Varnish, WordPress page-cache plugins, and Vercel edge all see this as a fresh fetch, so a fix you deployed five minutes ago cannot be hidden by a stale CDN entry.
  • Per-finding fetch metadata. Each issue records the UTC timestamp of the actual page.goto(), the post-redirect final URL, and the response's ETag / Last-Modified headers. Reproduce any finding with curl -H 'Cache-Control: no-cache' <final_url>.
  • Live-DOM verification. A small allow-list of transient-prone rules (duplicate-id, form structure, missing-alt) gets re-checked against the still-open page after engines finish. Findings the live DOM contradicts get a "Live DOM contradicted" chip and are routed to manual review instead of the headline count.
  • Result-cache TTL. Identical URL + WCAG level scans within 1 hour reuse our result cache; the cache key includes the backend version so deploys auto-invalidate. "Scan again" on the dashboard starts a fresh scan of the same URL, and every fresh scan re-fetches the target page.
  • Staging behind a firewall. If bot protection or an IP allow-list refuses the scan, the guide to allowing the scanner through your firewall lists its fixed addresses, User-Agent token and per-site header.