Prerequisites
You need three things before you start. The whole setup assumes you can serve your app at a local URL — if it runs in a browser, Alfa can audit it.
- Node 20+ — check with
node -v - Your app runs locally at a URL — note the start command and port (e.g.
npm run devon5173) - Code is in a GitHub repository with Actions enabled (the default)
The one thing you customize in this whole guide is Step 2 — how your app is served. Everything else is copy-paste.
Install dependencies
Three dev dependencies: the Playwright test runner, the Alfa audit engine, and the bridge between them. Then download the one browser Alfa audits in.
npm install -D @playwright/test @siteimprove/alfa-playwright @siteimprove/alfa-test-utils npx playwright install chromium
Using pnpm, yarn, or bun? Swap npm install -D for your equivalent.
Add playwright.config.ts
This boots your app before the audit and points the browser at it. Paste it into your project root, then change the two ⬅ lines to match your app.
import { defineConfig, devices } from "@playwright/test";
const PORT = 5173; // ⬅ your app's port
const BASE_URL = `http://localhost:${PORT}`;
export default defineConfig({
testDir: "./tests",
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html", { open: "never" }], ["list"]],
use: { baseURL: BASE_URL, trace: "on-first-retry" },
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "npm run dev", // ⬅ how to start YOUR app
url: BASE_URL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Pick your command and PORT:
| Stack | command | port |
|---|---|---|
| Vite | npm run dev | 5173 |
| Next.js | npm run build && npm run start | 3000 |
| Create React App | npm start | 3000 |
| Angular | npm start | 4200 |
| SvelteKit | npm run build && npm run preview | 4173 |
| Astro | npm run build && npm run preview | 4321 |
| Static files | npx serve . -l 5173 | 5173 |
webServer entirely and point baseURL at that URL instead.Add the audit test
This is the gate itself: render the page, hand the live DOM to Alfa, and fail if any rule reports a failure. It's the same for every framework.
import { test, expect } from "@playwright/test";
import { Playwright } from "@siteimprove/alfa-playwright";
import { Audit, Logging, Rules } from "@siteimprove/alfa-test-utils";
// Enforce WCAG 2.1 A/AA + Best Practices + ARIA. (Change scope at the bottom.)
const conformanceTarget: typeof Rules.wcag21aaFilter = (rule) =>
Rules.wcag21aaFilter(rule) ||
Rules.bestPracticesFilter(rule) ||
Rules.ARIAFilter(rule);
test("home page has no accessibility violations", async ({ page }) => {
await page.goto("/");
const alfaPage = await Playwright.toPage(await page.evaluateHandle("document"));
const result = await Audit.run(alfaPage, { rules: { include: conformanceTarget } });
Logging.fromAudit(result).print(); // readable report in the job log
const failing = result.resultAggregates.filter((a) => a.failed > 0);
expect(failing.size).toBe(0);
});
Guard more pages: copy the test(...) block and change the page.goto() path.
Add the npm script
One script so the same command runs locally and in CI.
{
"scripts": {
"test:a11y": "playwright test"
}
}
Run it locally
Confirm the gate works on your machine before wiring CI. Playwright starts your server, Alfa audits it, and the command exits non-zero on any violation.
npm run test:a11y
- It passes — or it lists real violations to fix (or narrow the scope in Step ±)
- Open the full report:
npx playwright show-report
Add the GitHub Actions workflow
This runs the exact same audit on every push and pull request, and uploads the HTML report as an artifact.
name: Accessibility
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: a11y-${{ github.ref }}
cancel-in-progress: true
jobs:
accessibility:
name: Accessibility audit (Alfa)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Install Playwright browser (Chromium)
run: npx playwright install --with-deps chromium
- name: Run Alfa accessibility audit
run: npm run test:a11y
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
The CI step uses --with-deps (installs Linux libraries); your local Step 1 doesn't need it.
Commit, push, and open a PR
Push it on a branch and open a pull request — the check runs on the PR automatically.
git checkout -b ci/alfa-a11y git add playwright.config.ts tests/accessibility.spec.ts package.json package-lock.json .github/workflows/accessibility.yml git commit -m "ci: add Alfa accessibility gate" git push -u origin ci/alfa-a11y
- Open a pull request into
mainand watch the Accessibility audit (Alfa) check run
Make the check required
A green check doesn't block anything on its own. To stop merges on violations, mark it required — in the GitHub UI, after the check has run once (it only appears in the list once it exists).
- Repo → Settings → Rules → Rulesets → New branch ruleset (or Settings → Branches → Add rule)
- Target branch: main
- Enable Require status checks to pass
- Search for and select Accessibility audit (Alfa)
- Enable Require a pull request before merging (recommended)
- Save
Prove the gate actually blocks
Worth doing once so you trust it. On a scratch branch, introduce a violation and watch CI go red.
<!-- image with no alt text — fails WCAG 1.1.1 --> <img src="logo.png">
- Push it → confirm the check turns red and the merge is blocked
- Delete the scratch branch — you're done ✅
Change the strictness
The audit's scope is just the conformanceTarget predicate from Step 3. Swap the filters to move the bar.
| Goal | Replace the body with |
|---|---|
| WCAG 2.1 AA only | Rules.wcag21aaFilter(rule) |
| Latest AA (WCAG 2.2) | Rules.aaFilter(rule) |
| Everything, incl. AAA | omit the rules option in Audit.run() |
Optional: publish results to Siteimprove
Everything above runs fully offline — the audit happens in your own CI runner and no page content or results leave your environment. You never need a Siteimprove account for the gate to work.
If your organisation does license the Siteimprove Intelligence Platform, you can additionally upload each audit to get dashboards and trends over time. This is purely opt-in and controlled by an API token.
Add three repository secrets under Settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
SI_USER_EMAIL | Your Siteimprove account email |
SI_API_KEY | Your Siteimprove API token |
SI_SITE_ID | The target site ID |
Pass them to the audit step in the workflow:
- name: Run Alfa accessibility audit
run: npm run test:a11y
env:
SI_USER_EMAIL: ${{ secrets.SI_USER_EMAIL }}
SI_API_KEY: ${{ secrets.SI_API_KEY }}
SI_SITE_ID: ${{ secrets.SI_SITE_ID }}
Then upload the result in the spec — only when all three are present:
import { Audit, Logging, Rules, SIP } from "@siteimprove/alfa-test-utils";
import { getCommitInformation } from "@siteimprove/alfa-test-utils/git";
const { SI_USER_EMAIL, SI_API_KEY, SI_SITE_ID } = process.env;
// No credentials? Skip the upload — the local gate still runs.
const reportUrl =
SI_USER_EMAIL && SI_API_KEY && SI_SITE_ID
? await SIP.upload(result, {
userName: SI_USER_EMAIL,
apiKey: SI_API_KEY,
siteID: Number(SI_SITE_ID),
commitInformation: await getCommitInformation(),
})
: undefined;
Logging.fromAudit(result, reportUrl).print(); // prints a link when uploaded
- Safe by default — if any secret is missing, the upload is skipped entirely and the pass/fail gate behaves exactly as before.
- Fork pull requests are unaffected — GitHub doesn't expose secrets to forks, so the upload simply doesn't run.
- Treat the API token as a credential — keep it in Actions secrets, never in the repo.