GitHub (opens in a new tab)
Mode
Themes
Color-vision friendly
High contrast

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

sh
npm install --save-dev @accessible-by-default/checks
TS
import { 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.