Checks
Playwright matchers that operate a page’s widgets by keyboard, in Chromium, Firefox and WebKit, and check that they behave as they claim. axe reads the markup. These press the button and look at what happened.
What they check
- Expanded state tells the truth
- A disclosure that says it is collapsed hides its panel.
toHaveTruthfulExpandedState- A dialog holds focus and gives it back
- Tab and Shift+Tab never reach the page behind a modal, and focus returns to the opener.
toOpenADialogThatHoldsFocus- Errors are tied to their fields
- Every invalid field has a message a screen reader can find from the field.
toHaveErrorsTiedToFields
Why they exist
In WebAIM’s screen reader survey #10 (December 2023 to January 2024), respondents ranked interactive elements like menus, tabs and dialogs that do not behave as expected second among problems, after CAPTCHA. The 2026 survey did not ask that question. Two of these checks cover part of that item: disclosures and dialogs.
axe reads markup, so it cannot press a button and look at what happened. These checks do that, and nothing else. Run them next to axe, not instead of it.
Use them
npm install --save-dev @accessible-by-default/checksimport { test } from '@playwright/test'
import { expect } from '@accessible-by-default/checks'
test('disclosures and popovers report their state truthfully', async ({ page }) => {
await page.goto('/settings')
await expect(page.locator('main')).toHaveTruthfulExpandedState()
})
test('the cart dialog holds focus and gives it back', async ({ page }) => {
await page.goto('/shop')
await expect(page.getByRole('button', { name: 'Cart' })).toOpenADialogThatHoldsFocus()
})
test('every error is tied to its field', async ({ page }) => {
await page.goto('/booking')
await page.getByRole('button', { name: 'Book' }).click()
await expect(page.locator('form')).toHaveErrorsTiedToFields()
})What they found here
This site runs all three on every change, in all three engines: every page, the craft and showcase dialogs, and the craft form after an empty submit. Read on 31 October 2026, with version 0.2.0:
- One warning, on purpose. Escape does not close the takeover dialog on the showcase, because
closedby="none"is the point of that demo. The run expects exactly that warning and nothing more. - One browser bug. Chromium put blank Tab stops behind the showcase’s dialogs, on the carousel’s scroll markers. The carousel now hides its markers while a modal dialog is open.
- Nothing else. No false expanded state, and nothing untied.
The sample is small. The run presses six different widgets, and no page here shows an error message of its own yet, so “nothing untied” means there was nothing to tie. That check waits for the first one.
What a pass does not prove
A pass is not conformance. The checks walk the keyboard only: a dialog can hold keyboard focus while a screen reader’s reading cursor wanders into the page behind. They do not check that a panel holds the right content, that a dialog has a name, or that an error message helps or is announced.
Everything else
The README ships with each version, and has every finding code, the engine notes and what each check leaves out. The package is on npm and GitHub, MIT licensed. A check that got a page wrong is the most useful report: open an issue with the smallest page that shows it.