| name | accessibility-testing |
| description | Test web applications for WCAG 2.1 AA accessibility compliance using automated scanning, screen reader testing, and keyboard navigation validation. Outputs axe-core integration, Playwright accessibility tests, and remediation guides. |
| argument-hint | ["compliance target (WCAG AA/AAA)","application type","user base","existing test framework"] |
| allowed-tools | Read, Write, Bash |
Accessibility Testing
Accessibility testing ensures your application is usable by people with disabilities — including those using screen readers, keyboard navigation, voice input, or high-contrast modes. WCAG 2.1 AA is the legal minimum in most jurisdictions and the ethical baseline for any public-facing product.
Process
- Automated scan first — axe-core catches 30-40% of accessibility issues automatically.
- Keyboard navigation testing — Tab through every interactive element, verify focus visibility and order.
- Screen reader testing — use NVDA+Firefox (Windows), VoiceOver+Safari (Mac/iOS), TalkBack (Android).
- Color contrast check — minimum 4.5:1 for normal text, 3:1 for large text.
- Manual WCAG checklist — cover areas automated tools miss (semantic HTML, alt text quality).
- Integrate into CI — fail builds on critical automated violations.
- Remediation and retest — fix issues by WCAG criterion, retest with tools + manual.
Output Format
Automated Testing with axe-core + Playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility - Core Pages', () => {
test('homepage passes WCAG 2.1 AA', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
await test.info().attach('axe-results', {
body: JSON.stringify(results, null, 2),
contentType: 'application/json',
});
expect(results.violations).toEqual([]);
});
test('product listing page', async ({ page }) => {
await page.();
results = ({ page })
.([, ])
.()
.();
blocking = results..(
[, ].(v.!)
);
(blocking. > ) {
.();
blocking.( {
.();
v..( .());
});
}
(blocking, ).();
});
(, ({ page }) => {
checkoutSteps = [
{ : , : },
{ : , : },
{ : , : },
];
allViolations = [];
( step checkoutSteps) {
page.(step.);
page.();
results = ({ page })
.([, , ])
.();
(results.. > ) {
allViolations.({
: step.,
: results.,
});
}
}
(allViolations, .(allViolations, , )).();
});
});
test.(, {
(, ({ page }) => {
page.();
page.();
page.();
focusedElement = page.( .?.);
(focusedElement)..();
bodyAriaHidden = page.(, );
results = ({ page })
.()
.();
(results.).();
page..();
(page.())..();
focusAfterClose = page.(
.?.()
);
(focusAfterClose).();
});
(, ({ page }) => {
page.();
page.();
emailError = page.();
(emailError).();
emailInput = page.();
ariaDescribedBy = emailInput.();
ariaErrorMessage = emailInput.();
ariaInvalid = emailInput.();
(ariaInvalid).();
(ariaDescribedBy || ariaErrorMessage).();
results = ({ page }).();
(results.).();
});
});
Keyboard Navigation Tests
import { test, expect } from '@playwright/test';
test.describe('Keyboard Navigation', () => {
test('can navigate entire header with keyboard', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const header = page.locator('header');
const interactiveElements = header.locator(
'a, button, input, select, [tabindex]:not([tabindex="-1"])'
);
const count = await interactiveElements.count();
for (let i = 0; i < count; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => ({
tag: document.activeElement?.tagName,
text: document.activeElement?.textContent?.().(, ),
: .?.() !== ,
}));
(focused.) {
focusedLocator = page.();
(focusedLocator).();
}
}
});
(, ({ page }) => {
page.();
page.().();
page..();
(page.()).();
page..();
firstItem = page.().();
(firstItem).();
page..();
secondItem = page.().();
(secondItem).();
page..();
(page.())..();
(page.()).();
});
(, ({ page }) => {
page.();
table = page.();
table.().().();
page..();
focusedInTable = page.(
.?.() !==
);
(focusedInTable).();
});
(, ({ page }) => {
page.();
page..();
skipLink = page.();
(skipLink).();
page..();
mainContent = page.();
focusedElement = page.();
focused = page.( ({
: .?.,
: .?.,
}));
([, ].(focused.) || focused. === ).();
});
});
Color Contrast Checker
from PIL import Image
import numpy as np
def luminance(r: int, g: int, b: int) -> float:
"""Calculate relative luminance (WCAG formula)."""
def linearize(c: float) -> float:
c = c / 255
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
def contrast_ratio(color1: tuple, color2: tuple) -> float:
"""Calculate WCAG contrast ratio between two RGB colors."""
l1 = luminance(*color1)
l2 = luminance(*color2)
lighter = max(l1, l2)
darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
def check_wcag_aa(foreground: tuple, background: tuple, large_text: bool = False) -> :
ratio = contrast_ratio(foreground, background)
threshold = large_text
{
: (ratio, ),
: threshold,
: ratio >= threshold,
: ratio >= ( large_text ),
: large_text,
}
() -> :
issues = []
text_colors = {k: v k, v tokens.items() k k}
bg_colors = {k: v k, v tokens.items() k k k}
text_name, text_color text_colors.items():
bg_name, bg_color bg_colors.items():
result = check_wcag_aa(
hex_to_rgb(text_color),
hex_to_rgb(bg_color)
)
result[]:
issues.append({
: text_name,
: bg_name,
: result[],
: result[],
: result[] - result[],
})
(issues, key= x: x[], reverse=)
() -> :
hex_color = hex_color.lstrip()
((hex_color[i:i+], ) i (, , ))
CI Integration
name: Accessibility Testing
on:
pull_request:
paths: ['src/**', 'public/**']
jobs:
axe-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm start &
- run: npx wait-on http://localhost:3000
- name: Run accessibility tests
run: npx playwright
WCAG Manual Checklist (Automated Tools Miss These)
## Perceivable
- [ ] All non-decorative images have meaningful alt text
- [ ] Decorative images have alt="" (empty alt)
- [ ] Videos have captions (auto-generated doesn't count)
- [ ] Audio content has transcripts
- [ ] Page doesn't rely on color alone to convey information
- [ ] Text can be resized to 200% without horizontal scroll
## Operable
- [ ] All functionality accessible by keyboard
- [ ] No keyboard traps (can always Tab out)
- [ ] Focus indicator is clearly visible
- [ ] Focus order is logical (matches visual reading order)
- [ ] Moving/auto-updating content can be paused
- [ ] No flashing content (risk of seizures: <3 flashes/second)
## Understandable
- [ ] `lang` attribute set correctly on `<html>`
- [ ] Form inputs have associated labels (not just placeholder text)
- [ ] Error messages describe what went wrong and how to fix it
- [ ] Errors are announced to screen readers (aria-live)
- [ ] Instructions don't rely solely on sensory characteristics ("click the red button")
## Robust
- [ ] HTML is valid (run W3C Validator)
- [ ] All interactive elements have accessible names
- [ ] Status messages announced via aria-live
- [ ] ARIA roles and attributes used correctly (don't add aria unless needed)
Rules
- Automated tests catch only 30-40% — they are necessary but not sufficient.
- Test with real assistive technology — not just automated tools; screen reader behavior differs.
- WCAG 2.1 AA is the legal baseline — WCAG 2.2 is current, but 2.1 AA is the minimum.
- Don't use
aria-* to fix broken HTML — fix the semantic HTML; ARIA is last resort.
- Placeholder text is not a label — it disappears on input; always use visible
<label>.
- Color contrast applies to icons, too — not just text; icons that convey information must meet 3:1.
- Focus indicators must be visible — browsers have defaults; many designs override them — don't.
- Block critical violations in CI —
critical and serious axe violations must not reach production.
- Test with keyboard only — unplug your mouse and complete each user journey; it exposes real problems.
- Include accessibility in design review — fixing accessibility during design is 10x cheaper than after build.