Understanding axe-core and Playwright

Published: | Author: Russ Weakley

What this article is about

This article is the starting point for the series. There's nothing to install and no code to run. It explains what axe-core and Playwright are, what each one can and can't test, and how they fit together. The tutorials that follow put each idea into practice.

If you work in accessibility, you've probably used an automated testing tool such as the axe DevTools browser extension. Developers often use these same tools as part of their development process, running accessibility checks as they work to catch problems before code is released.

They can be used separately, but they're especially useful together.

They can be used separately or together. And importantly, Playwright can test accessibility behaviour on its own, without axe-core.


axe-core

axe-core is an accessibility testing engine.

If you've used the axe DevTools browser extension, you've already used axe-core. It's the engine inside that extension, and inside many other accessibility tools, including Lighthouse.

axe-core runs inside the web page itself. You load a page, run axe-core on it, and it checks the rendered page for accessibility problems that can be detected automatically.

These include things such as:

At its simplest:

Page loads
↓
axe-core runs inside the page
↓
axe-core returns its results

These are the basic steps behind tools that use axe-core, whether it's a browser extension, a command line tool or a test script. The difference is how they run the checks and what they do with the results. You'll explore this by hand in Running axe-core in the browser console, and with a single command in Running axe-core from the command line.

Each problem axe-core finds is given an impact level: minor, moderate, serious or critical. This helps teams decide what to fix first. In Running axe-core with a Playwright script, you'll use impact levels to decide which issues should make a test fail.

One of axe-core's strengths is that it can check a whole page for a wide range of common accessibility problems at once, without anyone needing to write a separate test for each one.

axe-core also returns "incomplete" results alongside its violations. These are things it couldn't decide automatically and need a person to check. A common example is colour contrast for text over a background image or gradient. You'll explore these results in the browser console tutorial.

This is an important limitation of axe-core. It cannot determine every accessibility issue on its own.

It can't reliably answer questions such as:

So axe-core provides automated accessibility testing, not a complete accessibility audit.


Playwright

Playwright is a browser automation and testing framework.

It can control a browser much like a person would use a website. For example, Playwright can:

This means Playwright can get an application into the exact state you want to test. For example:

Open checkout page
↓
Add a product to the cart
↓
Open the cart dialog
↓
Run an accessibility test

This is one place where Playwright and axe-core fit together well. axe-core can inspect the current page, but it can't navigate through an application by itself. Playwright can do the navigation, then run axe-core inside the page it has opened. There's even an official package, @axe-core/playwright, that connects the two. You'll use it in Running axe-core with a Playwright script.

Playwright can also test accessibility behaviour directly, without running axe-core.

Playwright can test accessibility on its own

Playwright can work with accessibility information such as roles, accessible names and descriptions, much like the information exposed to assistive technologies.

For example, Playwright can find elements by their role and accessible name:

page.getByRole('button', { name: 'Submit' })

If the button doesn't expose the expected accessible name, Playwright won't find it, and the test fails. Just finding elements this way is already a small accessibility check.

axe-core would also flag a button with no accessible name at all. But only a test like this can confirm the name is the right one.

Playwright also has checks (called assertions) built specifically around accessibility, such as:

It can also take an ARIA snapshot. This is a simple text outline of how part of the page appears in the accessibility tree, which Playwright compares against what you expect.

Here's a complete accessibility test with no axe-core in it at all:

test('Escape closes the dialog and returns focus', async ({ page }) => {
  const trigger = page.getByRole('button', { name: 'Open settings' });
  await trigger.click();
  await expect(page.getByRole('dialog')).toBeVisible();

  await page.keyboard.press('Escape');

  await expect(page.getByRole('dialog')).toBeHidden();
  await expect(trigger).toBeFocused();
});

Even if you don't write code, you can probably read what this does: open the dialog, press Escape, check the dialog has closed, and check focus went back to the button that opened it.

This is functional accessibility testing: checking whether a component behaves as expected, rather than relying only on an automated scan to find accessibility problems. You'll find six worked examples like this in Accessibility testing with Playwright.


How they fit together

The two tools work at different layers. A useful mental model is:

Playwright
"Interact with the application and test the expected behaviour."
        ↓
Optional: axe-core (runs inside the page Playwright has opened)
"Check this state for automatically detectable accessibility problems."

Consider a registration form, where the email field should show an accessible error message when the form is submitted empty.

Playwright could:

Open the registration page
↓
Submit the form
↓
Find the email field
↓
Check the error message appears
↓
Check the error is connected to the field
↓
Check where focus moved

axe-core could then scan the resulting page for problems that can be detected automatically.

axe-core can't reliably perform all of those earlier checks. For example, if an error message isn't connected to the field, axe-core can't reliably determine which text on the page was intended to be the error. Playwright can check for the expected connection directly, using toHaveAccessibleErrorMessage or toHaveAccessibleDescription, depending on how the error is connected.

What about Cucumber?

Some teams add a third tool, Cucumber, which lets them write tests as plain-language scenarios ("Given... When... Then...") that run Playwright and axe-core behind the scenes. It's optional, and it isn't covered in this series. To learn more, read axe-core, Playwright and Cucumber.


Summary

If you only remember one thing:

Playwright can test accessibility behaviour on its own. axe-core adds a broad automated scan on top. Many teams use both.

Together, they can help move accessibility testing earlier into development, while also expanding testing well beyond a simple automated scan.

And neither of them replaces testing with real people, including people who use assistive technologies every day.


Next steps

Ready to see axe-core in action? Start with Running axe-core in the browser console. It needs no setup at all, just Google Chrome.