Skiplink

The GitHub Action

Runs axe-core against your URLs on every pull request and fails the build only on violations you have not already accepted.

# .github/workflows/a11y.yml
name: Accessibility
on: pull_request

jobs:
  a11y:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write        # for the report comment
    steps:
      - uses: actions/checkout@v4
      - uses: skiplink/action@v1
        with:
          urls: https://staging.example.com
          baseline: .skiplink/baseline.json
          token: ${{ secrets.SKIPLINK_TOKEN }}

That is the whole setup.

Create the baseline first

Without baseline: the gate fails on every violation it finds, which on an existing site means every build. Run this once, locally:

npx @skiplink/cli scan https://staging.example.com \
  --baseline .skiplink/baseline.json --update-baseline

git add .skiplink/baseline.json
git commit -m "Accept existing accessibility violations"

Read the diff before committing. Every entry is a violation the gate will now ignore, and reviewing that list is the entire point of keeping it in the repository.

What your team sees

Inputs

Action inputs
InputDefaultNotes
urlsRequired. One per line, or space-separated.
tokenRequired. Store as a repository secret.
baselinePath to the committed baseline file.
fail-onnewnew, critical, serious, moderate, minor, never
standardwcag22aaRule set. See the standards table.
wait-forCSS selector to wait for. For apps that render late.
commenttruePost and update the PR comment.
cli-versionlatestPin for reproducible builds.

Outputs

Action outputs
OutputNotes
passedtrue when the gate passed.
new-violationsCount of violations not in the baseline.
reportPath to the markdown report, for a later step.

Recipes

Several pages

      - uses: skiplink/action@v1
        with:
          urls: |
            https://staging.example.com/
            https://staging.example.com/pricing
            https://staging.example.com/checkout
          baseline: .skiplink/baseline.json
          token: ${{ secrets.SKIPLINK_TOKEN }}

Preview deployments

Baseline entries key on path rather than hostname, so a baseline recorded against one preview URL keeps matching on the next one. Feed it whatever URL your deploy step produced:

      - id: deploy
        run: ./deploy-preview.sh   # sets outputs.preview-url

      - uses: skiplink/action@v1
        with:
          urls: ${{ steps.deploy.outputs.preview-url }}
          baseline: .skiplink/baseline.json
          token: ${{ secrets.SKIPLINK_TOKEN }}

Warn without blocking

While a team gets used to it. The report still appears; nothing goes red.

      - uses: skiplink/action@v1
        continue-on-error: true
        with:
          fail-on: never
          urls: https://staging.example.com
          token: ${{ secrets.SKIPLINK_TOKEN }}

An app that renders late

      - uses: skiplink/action@v1
        with:
          urls: https://staging.example.com/app
          wait-for: "[data-testid=dashboard]"
          token: ${{ secrets.SKIPLINK_TOKEN }}

Notes

This is a composite action that runs the skiplink npm package. Everything it executes is readable in action.yml, rather than a committed dist/ bundle nobody reviews. It needs Node 20+, which ubuntu-latest already has.

A scan that could not run — site down, network blocked — exits with a code distinct from a gate failure and is reported as "could not complete", never as a pass.

What a green check means

No automatically detectable violations were found. Automated testing covers a subset of WCAG, so a green check is a floor, not a conformance claim. The long version.