Run axe-core accessibility tests against a running web app with Playwright, and post results as a PR comment.
On average, 57% of accessibility issues can be detected with automated testing, and fixing them as early as possible saves time and effort. Use this action to quick-start your accessibility journey and catch regressions before they reach production. Add 4 lines to your testing workflow, and get a detailed report of accessibility violations with links to the exact failing elements and rules documentation.
pnpm (≥ 9) must be available in the job before the action runs — the action's internal Playwright runner is a pnpm project. If your workflow doesn't already set it up:
- uses: pnpm/action-setup@v6
with:
version: "11"If your workflow already uses pnpm (e.g. to build your app), no extra step is needed.
# In your workflow — after your app is built and started:
- uses: pnpm/action-setup@v6
with:
version: "11"
- uses: leekeh/axtion@92dac76ca6d585203df89b544d037392c325f9d8 # v 0.0.1
with:
base-url: http://localhost:3000
routes: '["/", "/about", "/products"]'permissions:
pull-requests: write
contents: readIn the current iteration, a package.json with a packagemanager field pointing to a pnpm version is required. We are working on improving the ergonomics / flexibility of this part.
| Input | Description |
|---|---|
base-url |
Base URL of the running app, e.g. http://localhost:3000 |
routes or routes-file |
Routes to test — provide exactly one. See Routes format. |
| Input | Default | Description |
|---|---|---|
browser |
chromium |
Browser engine: chromium, firefox, or webkit |
workers |
2 |
Number of parallel Playwright workers |
wait-strategy |
networkidle |
Default page-readiness strategy. See Readiness strategies. |
exclusions |
[] |
JSON array of CSS selectors to exclude from axe scans |
These inputs let you restrict or expand which axe-core rules are run. By default none of them are set, so axe-core's built-in defaults apply (WCAG 2.x AA + best-practice rules).
| Input | Default | Description |
|---|---|---|
rulesets |
— | JSON array of rulesets associated with a specific standard that should be tested (e.g. '["wcag2aa", "RGAAv4"]'). See axe-core tags for the full list. |
rules |
— | JSON array of rule IDs to run exclusively. All other rules are skipped. E.g. '["color-contrast", "image-alt"]' |
disabled-rules |
— | JSON array of rule IDs to disable. Everything else stays active. E.g. '["color-contrast"]' |
| Input | Default | Description |
|---|---|---|
generate-report |
true |
Generate HTML reports for violations and upload as GitHub Actions artifacts |
post-comment |
true |
Post/update a PR comment with results. Only runs on pull_request events. |
github-token |
${{ github.token }} |
Token used for PR comments |
comment-template-success |
— | Path to a custom .md template for the success comment |
comment-template-failure |
— | Path to a custom .md template for the failure comment |
| Output | Description |
|---|---|
violations-found |
"true" if any violations were found |
report-url |
URL to the uploaded artifact (empty if tests passed or reporting is off) |
Routes can be passed as either the routes input (inline JSON string) or via a file with routes-file.
["/", "/about", "/search?q=bike"]Each route can be a string (uses the global wait-strategy) or an object. Only path is required:
[
"/",
{
"path": "/search?q=bike",
"name": "Search results"
},
{
"path": "/dashboard",
"name": "Dashboard",
"readiness": {
"type": "selector",
"selector": "#dashboard-content",
"timeout": 5000
}
},
{
"path": "/app",
"readiness": {
"type": "js",
"expression": "window.next !== undefined"
}
}
]The name field sets the test name shown in logs and in the PR comment. When omitted, a name is derived from the path.
| Type | Description | Extra fields |
|---|---|---|
networkidle |
Wait until no network requests for 500ms (default) | — |
load |
Wait for the load event |
— |
domcontentloaded |
Wait for DOMContentLoaded |
— |
selector |
Wait for a CSS selector to be visible | selector (required), timeout (ms, optional) |
js |
Poll a JS expression until truthy, then wait for requestIdleCallback |
expression (required), timeout (ms, optional) |
The js strategy is ideal for Next.js apps: "expression": "window.next !== undefined" detects when client-side hydration has started.
Override the default PR comment by providing a .md file with {{variable}} placeholders:
## My Custom A11y Report
Status: {{status}}
Affected routes:
{{affected_routes}}
[Download reports]({{report_url}})Available variables: {{status}}, {{report_url}}, {{report_url_section}}, {{affected_routes}}, {{timed_out_section}}, {{repo}}, {{pr_number}}
- uses: your-org/action-a11y-check@v1
with:
base-url: http://localhost:3000
routes: '["/"]'
rulesets: '["wcag21aa"]'- uses: your-org/action-a11y-check@v1
with:
base-url: http://localhost:3000
routes: '["/"]'
disabled-rules: '["color-contrast"]'- uses: your-org/action-a11y-check@v1
with:
base-url: http://localhost:3000
routes: |
[
"/",
{ "path": "/search", "name": "Search page" },
{
"path": "/products",
"name": "Products",
"readiness": { "type": "js", "expression": "window.next !== undefined" }
}
]- uses: your-org/action-a11y-check@v1
with:
base-url: http://localhost:4321
routes-file: tests/a11y/routes.json
exclusions: '["iframe", ".expressive-code"]'See CONTRIBUTING.md.
See CODE_OF_CONDUCT.md.