stableFor developers

Accessibility checks

The Playwright fixture can run an axe-core accessibility audit on the page after each screenshot. pixmoat compares the violations with the snapshot’s accessibility baseline, so a review shows which violations are new, and draws them on the screenshot.

How it works

  1. After each screenshot, the fixture runs @axe-core/playwright on the page that’s already loaded. The client installs it with the package, so there’s nothing extra to install.
  2. The violations and incomplete results are uploaded with the screenshot. If the audit throws an error, the screenshot is uploaded without results.
  3. pixmoat compares the violations with the snapshot’s accessibility baseline and gives the snapshot one result: New, Clean, Warnings or Violations.
  4. The review page shows the violations in the Accessibility tab and on the screenshot.
  5. When the review is complete, the results become the new accessibility baseline, except a Violations result nobody approved.

The audit runs in the same Playwright session as the visual capture. The Playwright reporter (beta) doesn’t audit.

A screenshot that’s byte-for-byte identical to its baseline carries no accessibility results, because the client skips uploading it. Set PIXMOAT_SKIP_MANIFEST=true in the Playwright process to send results for every screenshot. Performance and console explains this.

Turn on auditing

Two things decide whether a build has accessibility results:

  • The Playwright client audits only when .pixmoat.yaml has an accessibility section. Without the section it never runs axe, whatever the project settings say.
  • pixmoat evaluates the results only when accessibility is enabled for the build, from enabled: true in the file or, when the file leaves enabled out, from the Enable accessibility auditing toggle in Settings → Accessibility.

So turn it on in the file, and write enabled: true so both agree. Add an accessibility section to the .pixmoat.yaml in the directory you run the tests from:

version: 1

accessibility:
  enabled: true
  preset: wcag_aa          # contrast_only | wcag_aa | wcag_aaa
  gating: advisory         # advisory | blocking

The two cases people get wrong:

  • accessibility: {} turns the audit on. An empty section makes the client audit every snapshot. Only enabled: false inside the section turns it off. With an empty section, pixmoat still uses the toggle in Settings to decide whether to evaluate the results, so if the toggle is off the audits run and their results are dropped.
  • The toggle in Settings alone does nothing. Turning on accessibility in Settings without an accessibility section in the file gives you no results, because the client never audits.

The section is build-scoped: each commit carries its own settings, so a feature branch can turn auditing on before the default branch does. See Project configuration.

To skip the audit for one snapshot, for example a page with third-party embedded content, pass accessibility: false. The screenshot is still captured:

await pixmoat.snapshot(page, "third-party-widget", { accessibility: false });

Settings page

A project admin can change Settings → Accessibility. Its controls stay hidden until Enable accessibility auditing is checked. Not every control affects every build:

ControlEffect
Enable accessibility auditingEvaluates results for builds whose file leaves enabled out.
Audit presetShown on the project card only. The client picks the rules from the file’s preset, or wcag_aa when the file leaves it out.
Block review on accessibility violationsSets the gating mode. This is the one that’s enforced, whatever the file says. See Gating modes.
Max total violations, Max serious/critical violations, Fail on new violations, Regression detectionUsed for builds whose file leaves the matching key out.
Rule exclusionsHas no effect on the audit. The client reads rules.exclude from the file only.

When the latest default-branch build sent a .pixmoat.yaml file, the section shows a banner saying accessibility settings are managed by .pixmoat.yaml. The controls stay editable.

Presets

Presets control which rules axe-core checks. Set them with preset in the file:

PresetRulesUse case
contrast_onlycolor-contrast and link-in-text-blockStart narrow and catch contrast regressions only.
wcag_aa (default)The WCAG 2.0 and 2.1 Level A and AA rules: labels, landmarks, ARIA, focus, contrast and moreRecommended for most teams.
wcag_aaaThe wcag_aa rules plus Level AAA, including enhanced contrastStricter contrast requirements.

WCAG contrast ratios

axe-core checks these ratios, which WCAG defines:

LevelNormal text (<18pt / <14pt bold)Large text (>=18pt / >=14pt bold)
AA (SC 1.4.3)4.5:13:1
AAA (SC 1.4.6)7:14.5:1

Configuration reference

The full accessibility section:

accessibility:
  enabled: true                    # evaluate results for this build
  preset: wcag_aa                  # contrast_only | wcag_aa | wcag_aaa
  gating: advisory                 # advisory | blocking; the Settings value is enforced
  thresholds:
    max_violations: null           # most violations allowed in total (null = no limit)
    max_serious: null              # most serious or critical violations allowed
    fail_on_new: true              # a new violation needs review (default: true)
  regression_detection: true       # compare with the baseline to find new violations (default: true)
  rules:
    exclude: []                    # axe rule IDs to skip, for example ["region"]

.pixmoat.yaml reference lists every key.

Gating modes

ModeBehavior
advisory (default)Violations show in the review with a note that they don’t block it. There’s no Approve or Reject for them, so a Violations result never becomes the baseline. Start here.
blockingA Violations result needs an Approve or Reject decision in the Accessibility tab before the review can finish, and keeps a build open for review even when no screenshot changed. Finish review stays enabled, but finishing fails until every violation has a decision. A rejected one makes the build Rejected.

The mode that’s enforced is the one in Settings → Accessibility. A gating value in .pixmoat.yaml is stored with the build but doesn’t change what blocks the review, so set the mode in Settings. Waiting violations don’t change the commit status or the CI gate verdict; a rejected one does, because the build becomes Rejected. Performance and console explains the limits that apply to all three kinds of finding.

Thresholds

pixmoat checks each snapshot in this order and stops at the first match:

  1. The snapshot has no accessibility baseline: the result is New. Thresholds aren’t checked.
  2. max_violations: more violations than this in total gives Violations.
  3. max_serious: more serious and critical violations than this gives Violations.
  4. fail_on_new: any violation that isn’t in the baseline gives Violations. This only applies while regression_detection is on.
  5. No violations but some incomplete results gives Warnings.
  6. Otherwise the result is Clean, including when every violation is already in the baseline.

A threshold never fails a build by itself. It makes the result Violations, which needs a decision only in blocking mode.

Rule exclusions

Suppress rules that produce false positives in your project:

accessibility:
  enabled: true
  preset: wcag_aa
  rules:
    exclude:
      - region         # Landmark requirements on SPA layouts
      - meta-viewport  # Mobile viewport meta tag check

See the axe-core rule list for all rule IDs.

Baseline workflow

  1. First run: a snapshot without an accessibility baseline gets the result New. When the review is complete, its violations become the baseline without a decision.
  2. Later runs: each violation is matched with the baseline by its rule and the element it points at.
    • A violation that isn’t in the baseline is new, and with fail_on_new it needs review.
    • A violation that’s in the baseline and not in the build is shown as resolved.
    • A violation in both needs no action.
  3. Approving violations: in blocking mode, approving a Violations result makes the current violations the snapshot’s baseline on the build’s branch when the review is complete.

Accessibility baselines only change when someone finishes a review. A build that finishes as No changes doesn’t update them.

Branch fallback

A feature branch uses its own accessibility baseline when it has one, then the default branch’s, and otherwise the result is New. So a feature branch inherits the default branch’s accessibility baselines until a finished review on the branch writes its own.

Review UI

Build summary

When the build has accessibility results, the build summary shows a row such as:

Accessibility: 3 new violations 1 violations 2 warnings 12 clean

The first number counts new violations. The others count screenshots with each result. Select a count to open the matching screenshots.

When the project has accessibility settings but the build wasn’t evaluated, the row reads Accessibility: not evaluated for this build. This tells “turned off” apart from “turned on and found nothing”.

Accessibility tab

When the build has accessibility results, the review page has an Accessibility tab next to Visual. Press y to open it. Each violation card shows:

  • An impact badge: red for critical and serious, orange for moderate, yellow for minor
  • The rule ID, for example color-contrast, label or aria-required-attr
  • A NEW or RESOLVED badge when the violation is new or resolved compared with the baseline. Known violations have no badge.
  • The affected element’s HTML, and its CSS selector with Copy
  • axe-core’s explanation, a Learn more link to Deque University, and the WCAG criterion, for example SC 1.4.3
  • Show on screenshot, which highlights the element on the image

Contrast detail

For contrast violations, the card also shows:

  • The foreground and background colours
  • A ratio bar from 1:1 to 21:1, with markers at 3:1, 4.5:1 and 7:1
  • The current ratio and the ratio needed
  • The font size and weight, which decide the ratio needed
  • Suggested fix: an approximate grey foreground that would pass

Screenshot overlay

Violations whose element was found at capture time are outlined on the screenshot: red for critical and serious, amber for moderate and minor. Select an outline to scroll to its card. When the element couldn’t be found, the card shows Location unavailable and the violation is still recorded.

Capture light and dark themes

Accessibility problems often appear in only one colour scheme. Define themes in .pixmoat.yaml and the client captures and audits every snapshot once per theme. Each theme gets its own visual and accessibility baseline. Themes work without accessibility too. Connect Playwright shows the setup and the per-snapshot themes option.

Accessibility history

Project-level accessibility page

Select Accessibility in the project navigation to open Accessibility History. The page reads the latest approved build on the branch you pick. When the Enable accessibility auditing toggle in Settings is off, the page shows Accessibility auditing disabled, even if your file turns auditing on.

  • Accessibility Score from 0 to 100: 100 - (critical*10 + serious*5 + moderate*2 + minor*1), never below 0. It’s a trend indicator, not a compliance measure.
  • Snapshots and Violations by Impact counts
  • Violation Trends: Total Violations per Build and New Violations per Build
  • Top Rules (Latest Build), Top Violating Pages and Top Rules
  • A table of snapshots with their status, violations, new violations and trend

Compare branches

Select Compare branches and pick a branch to compare it with the branch you’re viewing. Branch Comparison shows the Score Change, New Violations, Resolved and the latest build counts for both branches.

Project card

On the projects page, each project card shows an accessibility pill with the project’s settings, such as Accessibility: off or Accessibility: on (wcag_aa, blocking). It reflects Settings, not the file. Select Accessibility: off to open the project’s accessibility settings.

Webhook payloads

A project admin can set a webhook under Settings → Webhooks. When a review is finished, pixmoat sends an HTTP POST with a JSON body to the Webhook URL. The event is build.approved or build.rejected. A build that finishes as No changes sends nothing.

Trigger on and Provider don’t change what’s sent yet: every finished review sends the same JSON payload to the URL.

Each request has these headers:

HeaderValue
Content-Typeapplication/json
User-Agentpixmoat-webhook/1.0
X-Pixmoat-Idempotency-KeyA unique ID for this delivery. Retries send the same one.
X-Pixmoat-SignatureOnly when a Secret is set: the hex-encoded HMAC-SHA256 of the raw request body, keyed with the secret.

Return a 2xx status within 30 seconds. Otherwise pixmoat retries after 30 seconds, then 1, 2 and 4 minutes, and gives up after five attempts.

The payload:

{
  "event": "build.rejected",
  "project": { "slug": "web", "name": "Web" },
  "build": {
    "id": "4f6c2c1e-8a0b-4e8e-9f55-2f1f3b8f7d10",
    "branch": "feat/dark-mode",
    "commit_sha": "9fceb02d0ae598e95dc970b74767f19372d61af8",
    "pr_number": 42,
    "ci_job_url": "https://gitlab.example.com/web/-/jobs/1234",
    "url": "https://pixmoat.com/p/web/builds/4f6c2c1e-8a0b-4e8e-9f55-2f1f3b8f7d10"
  },
  "review_url": "https://pixmoat.com/p/web/builds/4f6c2c1e-8a0b-4e8e-9f55-2f1f3b8f7d10",
  "approved_count": 3,
  "removed_count": 0,
  "flaky_count": 0,
  "quarantined_count": 0,
  "rejected_count": 1,
  "rejected_screenshots": [
    { "name": "settings", "viewport": "1280x720", "browser": "chromium", "comment": "Button overlaps the label" }
  ],
  "accessibility": {
    "score": 85,
    "total_violations": 22,
    "new_violations": 0,
    "resolved_violations": 0,
    "result": "clean"
  }
}
  • rejected_count and rejected_screenshots are only in build.rejected events. A rejected removal has "reason": "removed".
  • performance_regressions and performance_approved appear when the build has rejected or failed performance results, or approved degraded ones. console, console_findings and console_approved appear when the build has console results.

The accessibility object is present only when the build has accessibility results; otherwise the key is left out. result is the worst result across the build’s snapshots, from violations down to new, warnings and clean. resolved_violations is currently always 0.

Example: blocking on WCAG AA

A .pixmoat.yaml that audits against WCAG AA and captures light and dark themes:

version: 1

accessibility:
  enabled: true
  preset: wcag_aa
  thresholds:
    max_serious: 0           # any serious or critical violation needs review
    fail_on_new: true
  rules:
    exclude:
      - region               # SPA layout doesn't use landmarks

themes:
  light:
    media: "(prefers-color-scheme: light)"
  dark:
    media: "(prefers-color-scheme: dark)"

Then check Block review on accessibility violations in Settings → Accessibility.

  1. Start in advisory mode. Turn on auditing and see what violations exist without blocking reviews. The first finished review records each snapshot’s baseline.
  2. Fix real issues in code, such as contrast and missing labels.
  3. Switch to blocking in Settings once the baselines are clean. From then on, every Violations result needs a decision. Approve the ones you accept, so they become the baseline.

API endpoints

These endpoints accept a signed-in session or a personal access token, not a project access token. You need access to the project.

GET /api/projects/{slug}/accessibility/summary?branch=main

Returns the score, the result and impact counts, the top 10 violating pages and the top 10 rules from the latest approved build. branch is optional and defaults to the default branch.

GET /api/projects/{slug}/accessibility/history?branch=main&limit=50

Returns, for each snapshot, its recent accessibility results with the violation and new-violation counts, result and decision. branch is required. limit defaults to 50, up to 100. Add snapshot_key=<id> to return one snapshot.

GET /api/projects/{slug}/accessibility/compare?branch=feat/x&base=main

Returns the counts and score of both branches and the difference between them. Both parameters are required.

Troubleshooting

IssueSolution
No Accessibility tab in the reviewAdd an accessibility section with enabled: true to the .pixmoat.yaml in the directory you run the tests from. The settings toggle alone doesn’t make the client audit.
Not evaluated on the build pageAccessibility was turned off for that build. Check the file the build was created from, and the toggle in Settings if the file leaves enabled out.
Too many false positivesStart with preset: contrast_only, or add noisy rules to rules.exclude in the file.
A violation has no outline on the screenshotThe element couldn’t be located at capture time. The violation is still recorded.
Upload rejected with 400The accessibility results for one screenshot are over 100 KB. Exclude noisy rules, or pass accessibility: false for that page.
Violations never stop being reported as newIn advisory mode a Violations result can’t be approved, so it never becomes the baseline. Fix the violations, or switch to blocking mode and approve them.
Accessibility History says auditing is disabledThe page follows the toggle in Settings. Turn on Enable accessibility auditing there.