Accessibility Bug Hunt
A newsletter widget with 15 planted accessibility problems — missing labels, low contrast, keyboard traps and more — plus a fixed version to compare. Practice axe-core scans and keyboard tests in Playwright and Cypress; every recipe is run against this page in our test suite.
Practice area
A small newsletter widget with 15 accessibility problems planted in it. Find them with axe, your keyboard and a screen reader.
Join our newsletter
Offer ends Sunday — 20% off every course.
Popular topics
- Playwright
- Selenium
The 15 problems
Try to find them yourself first. axe finds 11 automatically; the other 4 need a keyboard or a screen reader — which is why automated scans are only the start.
1. Scan the broken version with axe
Run axe-core on just the practice area and list what it finds. It catches 11 of the 15 problems.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright'; // npm i -D @axe-core/playwright
test('axe finds the planted problems', async ({ page }) => {
await page.goto('/misc/accessibility');
const results = await new AxeBuilder({ page })
.include('[data-testid="a11y-area"]') // scan only the practice area
.analyze();
for (const v of results.violations) console.log(`${v.id} (${v.impact}): ${v.help}`);
const ids = results.violations.map((v) => v.id);
expect(ids).toEqual(expect.arrayContaining(['image-alt', 'label', 'color-contrast', 'button-name']));
});2. The fixed version passes
Switch to the fixed version and assert there are no violations — the check you'd keep in CI.
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('the fixed version has no axe violations', async ({ page }) => {
await page.goto('/misc/accessibility');
await page.getByRole('button', { name: 'Fixed version' }).click();
const results = await new AxeBuilder({ page }).include('[data-testid="a11y-area"]').analyze();
expect(results.violations).toEqual([]);
});3. Keyboard test — what axe can't see
Tab from the email field. In the fixed version focus lands on Subscribe and Enter submits; in the broken version Subscribe is a <div> that Tab skips.
import { test, expect } from '@playwright/test';
test('Subscribe works with the keyboard (fixed version)', async ({ page }) => {
await page.goto('/misc/accessibility');
await page.getByRole('button', { name: 'Fixed version' }).click();
const area = page.getByTestId('a11y-area');
await area.getByLabel('Email address').fill('asha@example.com');
await page.keyboard.press('Tab'); // → Topic
await page.keyboard.press('Tab'); // → Subscribe
await expect(area.getByRole('button', { name: 'Subscribe' })).toBeFocused();
await page.keyboard.press('Enter');
await expect(area.getByRole('status')).toHaveText('Subscribed asha@example.com');
});4. Dialog focus: in, trapped, Escape, back
A good dialog moves focus inside, closes with Escape and returns focus to the button that opened it.
import { test, expect } from '@playwright/test';
test('the offer dialog manages focus (fixed version)', async ({ page }) => {
await page.goto('/misc/accessibility');
await page.getByRole('button', { name: 'Fixed version' }).click();
const opener = page.getByRole('button', { name: 'View offer' });
await opener.click();
const dialog = page.getByRole('dialog', { name: 'Festive offer' });
await expect(dialog).toBeVisible();
await expect(dialog.getByRole('button', { name: 'Close' })).toBeFocused(); // focus moved in
await page.keyboard.press('Tab');
await expect(dialog.getByRole('link', { name: 'See the offer terms' })).toBeFocused();
await page.keyboard.press('Tab'); // wraps around
await expect(dialog.getByRole('button', { name: 'Close' })).toBeFocused();
await page.keyboard.press('Escape');
await expect(dialog).toBeHidden();
await expect(opener).toBeFocused(); // focus returned
});5. Errors must be text, not just colour
Submit a bad email. The fixed version marks the field invalid and links a readable message to it.
import { test, expect } from '@playwright/test';
test('an invalid email gives a readable error (fixed version)', async ({ page }) => {
await page.goto('/misc/accessibility');
await page.getByRole('button', { name: 'Fixed version' }).click();
const email = page.getByTestId('a11y-area').getByLabel('Email address');
await email.fill('not-an-email');
await page.getByTestId('a11y-area').getByRole('button', { name: 'Subscribe' }).click();
await expect(email).toHaveAttribute('aria-invalid', 'true');
await expect(email).toHaveAccessibleDescription('Enter a valid email address, like name@example.com.');
});