Smoketest

Playwright Accessibility Testing with ARIA Snapshots and Role Assertions

Playwright gives you three accessibility-aware ways to assert on a page: getByRole, ARIA snapshots, and visual snapshots. Here's what each one catches, when to use it, and where axe-playwright fills the gap.

6 min read

If you've ever written a getByRole locator, watched it fail, and not understood why, you've run into the thing most Playwright accessibility guides skip. These tools sound related but do different jobs, and using the wrong one for the job is most of the pain.

So here's the map. Playwright gives you three accessibility-aware tools that do different jobs. getByRole locates a single element by its role and accessible name. toMatchAriaSnapshot checks the structure of a whole section. The visual toMatchSnapshot catches pixel changes. For real WCAG compliance, you add axe-playwright on top. None of them replaces another.

The three tools, and the three jobs

ToolWhat it validatesFailure it catches
getByRole()A single element with the right role and accessible nameElement missing, renamed, or its role changed
toMatchAriaSnapshot()The structure of the accessibility tree for a sectionA section disappears, heading order changes, nav reorders
Visual toMatchSnapshot()Pixel-level appearanceCSS regressions, layout breaks

getByRole locates one element

getByRole finds an element the way a screen reader would: by its ARIA role and its computed accessible name, not by its HTML tag or placeholder text. That's the whole reason it survives a refactor. Change the placeholder and the locator still works, because it was never matching on the placeholder in the first place.

TypeScript
// Resilient: role plus accessible name
await page.getByRole('button', { name: 'Place order' }).click();

// Fragile: tied to the placeholder string
await page.getByPlaceholder('Card number').fill('4242424242424242');

Per the Playwright locators docs, you should usually pass the accessible name alongside the role, so the locator points at exactly one element. It catches an element being removed, an accessible-name change, or a role change, like a button that becomes a plain div. It misses structural changes, such as a whole nav section disappearing, and it says nothing about the elements near the one you targeted.

ARIA snapshots check structure

When you need to assert that a whole region is intact, not just one element, reach for ARIA snapshots, added in Playwright v1.50. toMatchAriaSnapshot() captures the accessibility tree of a locator as YAML and checks the page still matches it.

TypeScript
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
  - navigation:
    - link "Home"
    - link "Pricing"
    - link "Sign in"
`);

This catches nav items disappearing or reordering, heading hierarchy changing, an interactive element losing its role, a dialog that's no longer modal. It misses single-element failures (use getByRole for those) and anything visual. For content that legitimately changes, use partial matching: snapshot a stable section, or match a link with a regex so a rename doesn't fail the test.

Visual snapshots catch pixels

The visual form of toMatchSnapshot() is a pixel diff. Reach for it where exact rendering is the thing you care about: charts, marketing pages, dark mode, responsive breakpoints. Skip it for dynamic content, and expect maintenance, because font and rendering-engine differences between your machine and CI produce diffs that have nothing to do with a real regression. It's the highest-maintenance of the three.

What none of this does: WCAG compliance

Here's the part worth being honest about. None of the above tests accessibility compliance. ARIA snapshots and getByRole are accessibility-aware, in that they use the accessibility tree as their interface, but they don't check color contrast, screen-reader narration, focus management, or keyboard navigation. They will happily pass on a page that fails WCAG.

For an actual audit, add axe-playwright, which runs Deque's axe-core engine against the page:

TypeScript
import { injectAxe, checkA11y } from 'axe-playwright';

test('home page has no accessibility violations', async ({ page }) => {
  await page.goto('/');
  await injectAxe(page);
  await checkA11y(page);
});

Playwright's own accessibility testing guide walks through this. Treat it as a separate layer from the structural checks above, not a replacement for them.

The same accessibility tree now drives AI agents

Worth knowing if you work near AI tooling: the accessibility tree these assertions read is the same structure Playwright now exposes for AI agents to drive a browser. Recent versions tag each element in the tree with a ref, a stable handle the agent points at directly, instead of guessing a selector or hunting for the right text to match on. The ARIA snapshots docs cover the snapshot format and refs.

It's the same idea behind your getByRole locators, pushed one step further: the role-and-name view of a page is reliable enough to test against and to navigate by. It's why agent-driven tools read the accessibility tree rather than scrape the DOM. We dug into the token cost and trade-offs of that approach in running Playwright MCP in Claude Code.

The maintainability trade-off

Snapshots have a failure mode that single-element assertions don't. --update-snapshots is convenient and quietly dangerous. Run it on a real regression and you've just baked the bug into your baseline.

Treat an ARIA snapshot diff like a visual diff in review. An unexpected structural change is a failure to fix. An intentional UI change gets eyeballed, then the snapshot gets updated on purpose. The trap is auto-updating every snapshot on every PR because the diffs are noisy, at which point the tests have stopped telling you anything.

When to reach for each

SituationReach for
Locate and interact with a single elementgetByRole()
Assert an element's text or valuetoHaveText() / toHaveValue()
Validate a section's structuretoMatchAriaSnapshot()
Catch CSS or visual regressionsvisual toMatchSnapshot()
Check WCAG complianceaxe-playwright

Practical default: use getByRole for every locator and interaction, add ARIA snapshots on a few stable high-risk sections (nav, the main form, checkout), and use visual snapshots only where appearance is the point.

FAQ

What is accessibility testing in Playwright?

Playwright gives you three accessibility-aware assertions: getByRole locates an element by role and accessible name, toMatchAriaSnapshot() checks the structure of the accessibility tree for a section, and the visual toMatchSnapshot() checks pixels. They catch element-level, structural, and visual failures respectively. For WCAG compliance you add axe-playwright on top.

What is an ARIA snapshot in Playwright?

An ARIA snapshot is a YAML representation of the accessibility tree for a locator. toMatchAriaSnapshot() captures it once and then asserts the page still matches, so you find out when a section's structure regresses: a link disappears, headings reorder, an element loses its role. It was added in Playwright v1.50.

Why isn't my getByRole locator working?

Almost always because it's matching the wrong thing. getByRole matches on the ARIA role and the computed accessible name, not the HTML tag or the placeholder. If the accessible name is empty or comes from a different attribute than you expect, the locator won't find the element. Inspect the computed accessible name first, then write the locator to match it.

Do ARIA snapshots test WCAG compliance?

No. ARIA snapshots check structure only. They don't test color contrast, focus order, keyboard navigation, or screen-reader output, so a page can pass its snapshots and still fail WCAG. Use a dedicated tool like axe-playwright, which runs axe-core, for compliance auditing.

What's the difference between ARIA snapshots and visual snapshots?

ARIA snapshots assert structural correctness using the accessibility tree, which makes them resilient to environment differences like fonts and rendering engines. Visual snapshots assert pixel-level appearance, which catches CSS and layout regressions but breaks on harmless rendering differences between machines. Use ARIA snapshots for structure, visual snapshots only where exact appearance matters.

Which Playwright version added ARIA snapshots?

toMatchAriaSnapshot() landed in Playwright v1.50. Newer versions added refinements to the snapshot format, including element references used by AI agents, so check the official ARIA snapshots docs against the version you're running before relying on the exact YAML shape. Smoketest runs your real user tests in a cloud browser after every deploy, reading the same accessibility tree these assertions use, with no selectors or snapshots for you to keep current. See how it works.

Share this post

The QA column is no longer where the sprint goes to die.

Move one ticket. Watch it come back tested. Then decide.

Keep reading

All posts →