How to Check Website Accessibility with Automated Screenshots
Combine automated accessibility rules, accessibility-tree checks, and screenshots to review rendered pages. Learn what each catches, where it falls short, and how to make checks repeatable.
How do I check website accessibility with automated screenshots? Use a real browser to open a representative page and its relevant interactive states, run an automated accessibility rules scan against the rendered page, inspect its accessibility tree, and capture screenshots for visual review or regression comparison. A screenshot is useful evidence of appearance, but it cannot tell you whether a control has an accessible name, works with a keyboard, or makes sense to a screen reader.
This guide uses Playwright with axe for the do-it-yourself workflow. It shows how to scan a rendered page, capture a screenshot, inspect accessible structure, and run the checks in CI. Treat the results as evidence about the tested pages and states, not as proof that a site conforms to WCAG.
1. Know what each kind of evidence can tell you
| Evidence | What it observes | Useful for | Does not establish by itself |
|---|---|---|---|
| Automated rule scan, such as axe | Rule-testable properties of the rendered DOM | Finding some common issues such as missing labels, contrast problems, invalid properties, or duplicate IDs | That every WCAG requirement passed, every state was checked, or a person can complete the experience |
| Screenshot | Visual pixels at a particular viewport or across the page | Reviewing layout, visible content, charts or canvas appearance, and documenting a visual bug | Semantic structure, accessible names, keyboard behavior, or screen-reader output |
| Accessibility-tree or ARIA snapshot | Accessible roles, names, hierarchy, and relevant states | Asserting that controls expose expected structure and names | Visual clarity or every real assistive-technology interaction |
Use the three together. The screenshot gives visual context for a failure; the rules scan detects some machine-testable problems; and the accessibility tree exposes structure that pixels cannot show. Manual accessibility assessment and, where appropriate, assistive-technology and inclusive user testing remain necessary. Playwright’s documentation explicitly cautions that automated tests find some common problems while many accessibility problems require manual testing: Playwright accessibility testing.
2. Install Playwright and axe
Start with a Node.js project. The following installs Playwright Test, its browser binaries, and the Playwright integration for axe-core.
npm init -y
npm install --save-dev @playwright/test @axe-core/playwright
npx playwright install chromium
Use the same browser and operating-system environment for visual baselines and comparisons. If your project already has Playwright configured, add only the missing package and browser installation.
3. Write a test that scans a rendered state and saves evidence
Create tests/accessibility.spec.js. This runnable example assumes the app is already available at http://127.0.0.1:3000 and that its page has a button with the accessible name “Open navigation” which reveals a navigation element named “Main navigation.” Change the URL and locators to match your application. The test scans the revealed state, writes an accessibility-tree snapshot, captures a screenshot, and fails with axe violations.
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;
const fs = require('node:fs/promises');
const baseURL = process.env.BASE_URL || 'http://127.0.0.1:3000';
test('navigation state has no axe violations and has expected evidence', async ({ page }) => {
await page.goto(baseURL, { waitUntil: 'domcontentloaded' });
// Reveal the state that is part of the accessibility check.
await page.getByRole('button', { name: 'Open navigation' }).click();
const navigation = page.getByRole('navigation', { name: 'Main navigation' });
await expect(navigation).toBeVisible();
// Scan the rendered state. Axe includes a variety of rules by default.
const results = await new AxeBuilder({ page }).analyze();
const violations = results.violations.map((violation) => ({
id: violation.id,
impact: violation.impact,
help: violation.help,
helpUrl: violation.helpUrl,
nodes: violation.nodes.map((node) => ({
target: node.target,
html: node.html,
failureSummary: node.failureSummary,
})),
}));
await fs.mkdir('artifacts', { recursive: true });
await fs.writeFile('artifacts/axe-violations.json', JSON.stringify(violations, null, 2));
expect(violations, JSON.stringify(violations, null, 2)).toEqual([]);
// ARIA snapshot records accessible structure, not pixels.
const ariaSnapshot = await navigation.ariaSnapshot();
await fs.writeFile('artifacts/navigation.aria.txt', ariaSnapshot);
// Save both viewport and full-page evidence when useful.
await page.screenshot({ path: 'artifacts/navigation-viewport.png' });
await page.screenshot({ path: 'artifacts/navigation-full-page.png', fullPage: true });
});
Run it with npx playwright test tests/accessibility.spec.js. If the app needs to be started by the test runner, configure Playwright’s webServer and baseURL in playwright.config.js; for a quick local run, start the app in a separate terminal and set BASE_URL to its address.
A zero-violation result means only that axe reported no violations for the rules and rendered state actually scanned. Axe’s default set includes both WCAG-related and best-practice rules. If you make a claim specifically about WCAG, state the WCAG version and level you target, plus the rule tags or scope configured in the test. The standards reference here is WCAG 2.2; a tool’s rule selection is not a complete conformance assessment.
4. Cover pages, interactions, and states deliberately
One homepage scan is not coverage of a site. Select representative templates and critical user journeys, then identify states that can change the rendered interface or accessible structure. Examples include:
- Navigation closed and open; desktop and mobile menu states.
- Dialogs before and after opening, including focus behavior and dismissal.
- Forms in untouched, invalid, submitting, and success states.
- Expanded disclosures, tabs, filters, and dynamically loaded results.
- Authenticated and unauthenticated views, where relevant.
For each state, perform the user action, wait for the expected element, and then scan. A scan that runs before a flyout, error message, or dialog exists cannot report issues in that absent content. Prefer role and accessible-name locators so the test itself also depends on the semantics you expect.
For larger sites, keep a small inventory mapping each important template or flow to its checked states. Add a test when a state is introduced or changed, and avoid assuming that one scan generalizes to every route.
5. Inspect accessible structure with ARIA snapshots
Playwright’s ARIA snapshots represent the accessible structure of a page or locator. They can help you review and assert roles, names, hierarchy, and state. For example, after opening a menu, inspect the snapshot and compare the important expected structure:
const snapshot = await page.getByRole('navigation', { name: 'Main navigation' }).ariaSnapshot();
console.log(snapshot);
expect(snapshot).toContain('navigation "Main navigation"');
expect(snapshot).toContain('link "Products"');
Use assertions that reflect the intended interface, rather than freezing every incidental node in a large page. A role and name assertion can catch a missing or changed accessible label; it does not establish keyboard usability or correct screen-reader behavior in every browser and assistive-technology combination. See Playwright ARIA snapshots.
6. Use screenshot baselines for visual regression
Playwright Test’s toHaveScreenshot() creates a reference image on its first run and compares later captures against it. Playwright waits for consecutive matching screenshots before saving a baseline. Keep the baseline in version control and review changes rather than accepting them automatically.
const { test, expect } = require('@playwright/test');
test('navigation has a stable visual appearance', async ({ page }) => {
await page.goto(process.env.BASE_URL || 'http://127.0.0.1:3000');
await page.getByRole('button', { name: 'Open navigation' }).click();
await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible();
await expect(page).toHaveScreenshot('navigation-open.png', {
fullPage: true,
animations: 'disabled',
maxDiffPixelRatio: 0.002,
});
});
The threshold above is an example setting, not a universal accessibility tolerance. Tune it for the project, inspect diffs, and do not interpret tolerated pixel changes as an accessibility judgment. A changed image can come from a product change, a font or browser update, a changed rendering environment, or a genuine regression.
Rendering varies with operating system, browser version, settings, hardware, power state, and headless mode. Generate and compare baselines in a consistent environment, such as the same CI image and browser version. Dynamic timestamps, rotating promotions, avatars, and other volatile content can make snapshots noisy. Use a screenshot stylesheet to stabilize or hide genuinely irrelevant volatility, but do not filter the content being reviewed. Playwright documents visual comparisons and snapshot options at Visual comparisons.
7. Run checks in CI and report their scope
Run the same representative state tests on pull requests or another regular CI schedule. Save the axe output, ARIA snapshots, and screenshot diffs as artifacts when a run fails. Keep browser versions and the rendering image stable, and review proposed baseline updates with the associated code change.
In the test report or team documentation, record:
- Which routes, templates, and user states were tested.
- Which browser and environment produced visual baselines.
- Which axe rules or tags were in scope, including the WCAG version and level when making a WCAG-specific statement.
- What was assessed manually, including keyboard and assistive-technology checks appropriate to the product.
This makes a passing run interpretable: it says which automated rules ran on which pages and states, alongside the visual and structural evidence collected. It should not be phrased as a site-wide guarantee of accessibility.
8. Where automated screenshots and scans fall short
- Pixels do not expose semantics. A visually clear icon may have no accessible name; a screenshot cannot reveal that omission.
- A DOM scan does not operate the product like a person. It may not determine whether a workflow is understandable, whether keyboard focus is usable, or whether an assistive-technology interaction works as intended.
- Unvisited states remain untested. A closed dialog or unexpanded menu is not covered just because the page was scanned once.
- Automated rule coverage is bounded. Axe can identify common machine-testable problems but cannot detect every WCAG issue. A clean report is not conformance evidence by itself.
- Visual diffs are not accessibility scores. They show pixel changes. A change can be harmless or important; human review must determine which.
Use automation to find issues earlier and make review repeatable. Add manual assessment and user testing to determine whether people can perceive, understand, navigate, and operate the experience.
9. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| The test cannot find the menu button | The locator does not match the app’s accessible role/name, or the page has not finished rendering. | Inspect the actual accessible name, update the locator to the intended control, and wait for the page-specific ready condition. |
| The scan reports no issues, but a known dialog is missing | The scan ran before the interaction exposed the dialog. | Trigger the interaction, wait for the dialog to be visible, then scan that state separately. |
| Axe fails on a page with a third-party widget | The widget contributes violations or changes asynchronously. | Review the violation target and determine whether the widget is in scope. If a region is intentionally excluded, document the reason and assess it separately; do not suppress findings just to obtain a pass. |
| Screenshot comparisons fail across machines | Different OS, browser build, fonts, rendering settings, or headless mode. | Generate and compare baselines in the same pinned CI environment and browser version. |
| Screenshot diffs change on every run | Animations, time, randomized data, rotating content, or remote assets vary. | Control test data and time where possible, disable animations, wait for stable content, and use a screenshot stylesheet only for irrelevant volatile regions. |
| A tiny pixel change fails the visual assertion | The selected diff threshold is too strict for the environment, or the change is real. | Inspect the diff first. Stabilize the environment and deliberately tune the threshold; do not raise it until meaningful regressions disappear. |
| The test passes but keyboard use is broken | Axe and screenshots do not fully evaluate interaction behavior. | Add keyboard-focused checks and manual testing with the relevant assistive technologies. |
| The ARIA snapshot assertion breaks after a harmless page change | The assertion covers too much incidental structure. | Assert the roles, names, and hierarchy that matter to the interaction rather than an entire volatile tree. |
10. Performance, reliability, and cost
Accessibility scans and screenshots both add browser work to a test run. Keep the suite useful by selecting representative templates and meaningful states, rather than scanning every route-state combination without a coverage reason. Run a focused set on each change and schedule broader coverage when CI time is constrained. Reuse the same page state for the scan, ARIA snapshot, and screenshot where that produces the evidence you need.
Reliability depends on repeatable inputs: deterministic test data, explicit waits for the state under review, pinned browser and operating-system environments for visual baselines, and deliberate review of updated snapshots. A network-idle wait is not always the right readiness condition for applications with long-lived requests; waiting for the specific control or content under test is often more meaningful.
The Playwright and axe workflow uses open-source packages, but it still has engineering and CI compute costs. The research does not establish universal runtime or cost benchmarks. Budget based on your own routes, state coverage, browser matrix, and artifact retention. If you also need shareable screenshots outside a browser test suite, the separate ScreenshotNeo option below provides a screenshot API; it does not replace the DOM scan, accessibility-tree inspection, or manual assessment.
Or skip the browser setup
For screenshot capture, ScreenshotNeo is a website screenshot API and MCP server. It does not run an accessibility audit, so keep the Playwright and axe checks for DOM rules and accessible structure. A single GET request captures a URL as an image or PDF; the docs list the available parameters at ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. For accessibility work, use its screenshots as visual evidence alongside your browser-based rule scan, accessibility-tree checks, and human review.
Sign up for 1,000 free screenshots a month with no card.
FAQ
Can a screenshot test accessibility?
It can help review visual presentation and document regressions, but it cannot test accessible names, semantic structure, keyboard operation, or screen-reader output by itself.
Does a zero-violation axe scan mean a page conforms to WCAG?
No. It means the configured rules reported no violations in the rendered state that was scanned. State the tested scope and add manual assessment.
Should I use viewport or full-page screenshots?
Use a viewport capture for the initially visible layout or a specific interaction. Use full-page capture when below-the-fold layout is relevant; neither format exposes semantics.
Do I need a screenshot API to run accessibility checks?
No. Playwright can capture screenshots in the same browser workflow that runs axe and inspects ARIA snapshots. A screenshot API is useful for capture workflows, but it does not substitute for those checks.


