This episode covers automatic accessibility checks with axe-core integration, validating ARIA roles, labels, and keyboard navigation, testing contrast and focus state, as well as adding accessibility assertions to the regression suite.

An application that can't be used by people with disabilities isn't just an ethical problem — it's a business problem, and increasingly a legal one. This episode 17 covers accessibility testing: ensuring the application can be navigated and understood by everyone, including screen reader and keyboard-only users.
Two approaches complement each other here. axe-core provides an automatic audit against standardized accessibility rules, while Playwright assertions explicitly verify ARIA structure, focus state, and keyboard flows. Both can become part of a regression suite that runs every day.
@axe-core/playwright provides direct integration with a Playwright page:
npm install -D @axe-core/playwrightThis package exposes the AxeBuilder API, which scans the page and returns a list of violations.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('login page has no accessibility violations', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});new AxeBuilder({ page }).analyze() scans the page and returns results.violations — a list of violations with their impact and fixes. The assertion expect(results.violations).toEqual([]) forces the page to be free of violations.
Each violation contains complete information: rule id, impact (critical, serious, moderate), description, and the offending nodes. Don't rush to disable rules; it's better to fix the root cause. For violations genuinely irrelevant to the application's context, use disableRules with a clear note.
Playwright assertions work extremely well with ARIA because getByRole and the accessible name are at their core. You can verify the accessibility structure directly:
import { test, expect } from '@playwright/test';
test('main navigation has a navigation role', async ({ page }) => {
await page.goto('/home');
const nav = page.getByRole('navigation');
await expect(nav).toBeVisible();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});page.getByRole('navigation') finds elements with the navigation role. If the main navigation doesn't use <nav> or role="navigation", this assertion fails — exactly what you want from an accessibility test.
Form inputs without labels are a very common violation. Asserting a textbox role accessed via the correct name enforces this rule:
await page.getByLabel('Email Address').fill('user@example.com');
await page.getByRole('textbox', { name: 'Password' }).fill('secret123');getByLabel('Email Address') only finds an input if there's a label genuinely associated with it — either via a <label> element or the aria-label attribute. This test indirectly verifies form accessibility.
Keyboard-only users navigate with Tab and Enter. Playwright can simulate this flow:
test('login can be completed using only the keyboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').focus();
await page.keyboard.press('Tab');
await page.keyboard.type('secret123');
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/dashboard/);
});page.keyboard.press('Tab') moves focus to the next element in the tab order. The keyboard flow above verifies that the form can be filled and submitted without a mouse at all.
Interactive elements must have a visible focus indicator. A simple way to verify it is to focus and then check whether the style changes — or more practically, ensure the element receives focus and can be used:
await page.getByRole('link', { name: 'Menu' }).focus();
await expect(page.getByRole('link', { name: 'Menu' })).toBeFocused();toBeFocused() ensures the element receives focus. Combined with a keyboard flow, this confirms that interactive elements can genuinely be focused, not just exist in the DOM.
Color contrast is a complex rule that depends on color calculations. Although it can be checked manually, the most reliable way is through the axe rules (for example, color-contrast) already enabled in the automatic audit. Make sure the contrast rule isn't disabled.
Semantic structure — correct headings, lists, and buttons that aren't divs — is the foundation of accessibility. The axe audit covers many of these rules automatically. As an extra layer, structure can be asserted explicitly:
const headings = page.getByRole('heading');
expect(await headings.count()).toBeGreaterThan(0);
await expect(headings.first()).toHaveText('Dashboard');Ensuring a page has proper headings helps screen reader users navigate the document. Automatic audits plus explicit assertions give you two layers of defense.
Running an axe audit on every page adds time to the suite. A healthy strategy: run the full audit on core pages (login, checkout, home), not on every small page. Focus on critical flows first, then expand coverage gradually.
Visual snapshots and accessibility audits are both sensitive to content. Use stable mock data (episode 12) so audit results are consistent on every run — not random data that changes the number of offending nodes.
Wrap the audit in a helper so it's easy to reuse and consistent:
import AxeBuilder from '@axe-core/playwright';
export async function assertAccessible(page) {
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
}assertAccessible(page) scans the page and throws an error if there are violations. This one line can be called at the end of every important test, keeping the accessibility layer always guarded.
npx playwright test tests/accessibility.spec.tsEpisode 17 made accessibility part of the definition of "done": automatic axe-core audits detect standard violations, role and label assertions verify the ARIA structure, keyboard flows test mouse-free navigation, and the assertAccessible helper integrates all of it into the regression suite that runs every day.
Key takeaways:
AxeBuilder provides automatic audits against standard accessibility rules.getByRole and getByLabel naturally verify ARIA roles and labels.keyboard.press for mouse-free flows.In the next episode we'll discuss custom tooling and extensions — creating custom test fixtures and helpers, extending Playwright with plugins and reporters, integrating with test utilities and codegen scripts, and sharing utilities across projects.