
Accessibility Testing
- 513 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
accessibility-testing is a Claude skill from aj-geddes/useful-ai-prompts that runs WCAG audits, keyboard navigation checks, screen reader flows, and remediation plans for web and mobile surfaces before release.
About
accessibility-testing is a Claude skill in aj-geddes/useful-ai-prompts that guides developers through inclusive QA before release. It structures WCAG conformance reviews, keyboard-only navigation checks, screen reader user flows, and actionable remediation plans for web and mobile UI. The skill fits ship-phase testing when compliance, audit readiness, or user inclusivity block a launch. Developers reach for accessibility-testing when a PR needs a11y coverage beyond lint rules, when preparing for WCAG-driven reviews, or when screen reader and keyboard paths have not been validated on real components.
- WCAG conformance checklist prompts
- Keyboard and focus order testing
- Screen reader flow simulation
- ARIA and semantic HTML review
- Remediation prioritization by severity
Accessibility Testing by the numbers
- 513 all-time installs (skills.sh)
- Ranked #611 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill accessibility-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 513 |
|---|---|
| repo stars | ★ 305 |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you audit web apps for WCAG compliance?
Run WCAG audits, keyboard navigation checks, screen reader flows, and remediation plans before release so web and mobile surfaces stay inclusive and compliant.
Who is it for?
Frontend and mobile developers preparing release candidates who need structured WCAG, keyboard, and screen reader test coverage.
Skip if: Backend-only API projects with no user interface or teams already running automated axe-core CI with full coverage.
When should I use this skill?
A web or mobile release needs WCAG review, keyboard navigation validation, or screen reader flow testing before launch.
What you get
WCAG audit findings, keyboard navigation reports, screen reader flow results, and remediation plans
- WCAG audit report
- remediation plan
- keyboard flow checklist
Files
Accessibility Testing
Table of Contents
Overview
Accessibility testing ensures web applications are usable by people with disabilities, including those using screen readers, keyboard navigation, or other assistive technologies. It validates compliance with WCAG (Web Content Accessibility Guidelines) and identifies barriers to accessibility.
When to Use
- Validating WCAG 2.1/2.2 compliance
- Testing keyboard navigation
- Verifying screen reader compatibility
- Testing color contrast ratios
- Validating ARIA attributes
- Testing form accessibility
- Ensuring focus management
- Testing with assistive technologies
Quick Start
Minimal working example:
// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Homepage Accessibility", () => {
test("should not have any automatically detectable WCAG A or AA violations", async ({
page,
}) => {
await page.goto("/");
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test("navigation should be accessible", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).include("nav").analyze();
expect(results.violations).toEqual([]);
});
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| axe-core with Playwright | axe-core with Playwright |
| Keyboard Navigation Testing | Keyboard Navigation Testing |
| ARIA Testing | ARIA Testing |
| Jest with jest-axe | Jest with jest-axe |
| Cypress Accessibility Testing | Cypress Accessibility Testing |
| Python with Selenium and axe | Python with Selenium and axe |
Best Practices
✅ DO
- Test with real assistive technologies
- Include keyboard-only users
- Test color contrast
- Use semantic HTML
- Provide text alternatives
- Test with screen readers
- Run automated tests in CI
- Follow WCAG 2.1 AA standards
❌ DON'T
- Rely only on automated tests (they catch ~30-40% of issues)
- Use color alone to convey information
- Skip keyboard navigation testing
- Forget focus management in dynamic content
- Use div/span for interactive elements
- Hide focusable content with display:none
- Ignore ARIA best practices
- Skip manual testing
ARIA Testing
ARIA Testing
// tests/accessibility/aria.test.ts
import { test, expect } from "@playwright/test";
test.describe("ARIA Attributes", () => {
test("buttons should have accessible names", async ({ page }) => {
await page.goto("/");
const buttons = await page.locator("button").all();
for (const button of buttons) {
const text = await button.textContent();
const ariaLabel = await button.getAttribute("aria-label");
const ariaLabelledBy = await button.getAttribute("aria-labelledby");
expect(
text?.trim() || ariaLabel || ariaLabelledBy,
"Button has no accessible name",
).toBeTruthy();
}
});
test("icons should have aria-hidden or labels", async ({ page }) => {
await page.goto("/");
const icons = await page
.locator('[class*="icon"], svg[class*="icon"]')
.all();
for (const icon of icons) {
const ariaHidden = await icon.getAttribute("aria-hidden");
const ariaLabel = await icon.getAttribute("aria-label");
const title = await icon.locator("title").count();
// Icon should be hidden from screen readers OR have a label
expect(
ariaHidden === "true" || ariaLabel || title > 0,
"Icon without aria-hidden or accessible name",
).toBeTruthy();
}
});
test("custom widgets should have correct roles", async ({ page }) => {
await page.goto("/components");
// Tab widget
const tablist = page.locator('[role="tablist"]');
await expect(tablist).toHaveCount(1);
const tabs = tablist.locator('[role="tab"]');
const tabpanels = page.locator('[role="tabpanel"]');
expect(await tabs.count()).toBeGreaterThan(0);
expect(await tabs.count()).toBe(await tabpanels.count());
// Check aria-selected
const selectedTab = tabs.locator('[aria-selected="true"]');
await expect(selectedTab).toHaveCount(1);
// Check tab associations
const firstTab = tabs.first();
const ariaControls = await firstTab.getAttribute("aria-controls");
const associatedPanel = page.locator(`[id="${ariaControls}"]`);
await expect(associatedPanel).toHaveCount(1);
});
test("live regions announce changes", async ({ page }) => {
await page.goto("/");
// Find live region
const liveRegion = page.locator('[role="status"], [aria-live]');
// Trigger update
await page.click('[data-testid="load-data"]');
// Wait for content
await liveRegion.waitFor({ state: "visible" });
const ariaLive = await liveRegion.getAttribute("aria-live");
expect(["polite", "assertive"]).toContain(ariaLive);
});
});axe-core with Playwright
axe-core with Playwright
// tests/accessibility/homepage.a11y.test.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Homepage Accessibility", () => {
test("should not have any automatically detectable WCAG A or AA violations", async ({
page,
}) => {
await page.goto("/");
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test("navigation should be accessible", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page }).include("nav").analyze();
expect(results.violations).toEqual([]);
});
test("form should be accessible", async ({ page }) => {
await page.goto("/contact");
const results = await new AxeBuilder({ page })
.include("form")
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
// Additional form checks
const inputs = await page.locator("input, select, textarea").all();
for (const input of inputs) {
const id = await input.getAttribute("id");
const ariaLabel = await input.getAttribute("aria-label");
const ariaLabelledBy = await input.getAttribute("aria-labelledby");
// Every input should have an associated label
if (id) {
const label = await page.locator(`label[for="${id}"]`).count();
expect(
label > 0 || ariaLabel || ariaLabelledBy,
`Input with id="${id}" has no associated label`,
).toBeTruthy();
}
}
});
test("images should have alt text", async ({ page }) => {
await page.goto("/");
const images = await page.locator("img").all();
for (const img of images) {
const alt = await img.getAttribute("alt");
const role = await img.getAttribute("role");
const ariaLabel = await img.getAttribute("aria-label");
// Decorative images should have empty alt or role="presentation"
// Content images must have descriptive alt text
expect(
alt !== null || role === "presentation" || ariaLabel,
"Image missing alt text",
).toBeTruthy();
}
});
test("color contrast should meet AA standards", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["cat.color"])
.analyze();
expect(results.violations).toEqual([]);
});
});Cypress Accessibility Testing
Cypress Accessibility Testing
// cypress/e2e/accessibility.cy.js
describe("Accessibility Tests", () => {
beforeEach(() => {
cy.visit("/");
cy.injectAxe();
});
it("has no detectable a11y violations on load", () => {
cy.checkA11y();
});
it("navigation is accessible", () => {
cy.checkA11y("nav");
});
it("focuses on first error when form submission fails", () => {
cy.get("form").within(() => {
cy.get('[type="submit"]').click();
});
cy.focused().should("have.attr", "aria-invalid", "true");
});
it("modal has correct focus management", () => {
cy.get('[data-cy="open-modal"]').click();
// Focus should be in modal
cy.get('[role="dialog"]').should("exist");
cy.focused().parents('[role="dialog"]').should("exist");
// Close modal with Escape
cy.get("body").type("{esc}");
cy.get('[role="dialog"]').should("not.exist");
// Focus returns to trigger
cy.get('[data-cy="open-modal"]').should("have.focus");
});
});Jest with jest-axe
Jest with jest-axe
// tests/components/Button.a11y.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from '../Button';
expect.extend(toHaveNoViolations);
describe('Button Accessibility', () => {
test('should not have accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('icon button should have aria-label', async () => {
const { container } = render(
<Button aria-label="Close">
<CloseIcon />
</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('disabled button should be accessible', async () => {
const { container } = render(<Button disabled>Disabled</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});Keyboard Navigation Testing
Keyboard Navigation Testing
// tests/accessibility/keyboard-navigation.test.ts
import { test, expect } from "@playwright/test";
test.describe("Keyboard Navigation", () => {
test("should navigate through focusable elements with Tab", async ({
page,
}) => {
await page.goto("/");
// Get all focusable elements
const focusableSelectors =
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
const focusableElements = await page.locator(focusableSelectors).all();
// Tab through all elements
for (let i = 0; i < focusableElements.length; i++) {
await page.keyboard.press("Tab");
const focusedElement = await page.evaluate(() => {
return {
tagName: document.activeElement?.tagName,
id: document.activeElement?.id,
className: document.activeElement?.className,
};
});
expect(focusedElement.tagName).toBeTruthy();
}
});
test("should skip navigation with skip link", async ({ page }) => {
await page.goto("/");
// Tab to skip link (usually first focusable element)
await page.keyboard.press("Tab");
const skipLink = await page.locator(".skip-link");
await expect(skipLink).toBeFocused();
// Activate skip link
await page.keyboard.press("Enter");
// Focus should be on main content
const focusedElement = await page.evaluate(() => {
return document.activeElement?.id;
});
expect(focusedElement).toBe("main-content");
});
test("modal should trap focus", async ({ page }) => {
await page.goto("/");
// Open modal
await page.click('[data-testid="open-modal"]');
await page.waitForSelector('[role="dialog"]');
const modal = page.locator('[role="dialog"]');
const focusableInModal = modal.locator(
"a[href], button, input, select, textarea",
);
const count = await focusableInModal.count();
// Tab through all elements in modal
for (let i = 0; i < count + 2; i++) {
await page.keyboard.press("Tab");
}
// Focus should still be within modal
const focusedElement = await page.evaluate(() => {
const modal = document.querySelector('[role="dialog"]');
return modal?.contains(document.activeElement);
});
expect(focusedElement).toBe(true);
});
test("dropdown menu should be keyboard accessible", async ({ page }) => {
await page.goto("/");
// Navigate to dropdown trigger
await page.keyboard.press("Tab");
const dropdown = page.locator('[data-testid="dropdown-menu"]');
await dropdown.focus();
// Open dropdown with Enter
await page.keyboard.press("Enter");
// Menu should be visible
const menu = page.locator('[role="menu"]');
await expect(menu).toBeVisible();
// Navigate menu items with arrow keys
await page.keyboard.press("ArrowDown");
const firstItem = menu.locator('[role="menuitem"]').first();
await expect(firstItem).toBeFocused();
await page.keyboard.press("ArrowDown");
const secondItem = menu.locator('[role="menuitem"]').nth(1);
await expect(secondItem).toBeFocused();
// Escape should close menu
await page.keyboard.press("Escape");
await expect(menu).not.toBeVisible();
await expect(dropdown).toBeFocused();
});
test("form can be completed using keyboard only", async ({ page }) => {
await page.goto("/contact");
// Tab to first field
await page.keyboard.press("Tab");
await page.keyboard.type("John Doe");
// Tab to email field
await page.keyboard.press("Tab");
await page.keyboard.type("john@example.com");
// Tab to message
await page.keyboard.press("Tab");
await page.keyboard.type("Test message");
// Tab to submit and activate
await page.keyboard.press("Tab");
await page.keyboard.press("Enter");
// Check form was submitted
await expect(page.locator(".success-message")).toBeVisible();
});
});Python with Selenium and axe
Python with Selenium and axe
# tests/test_accessibility.py
import pytest
from selenium import webdriver
from axe_selenium_python import Axe
class TestAccessibility:
@pytest.fixture
def driver(self):
"""Setup Chrome driver."""
options = webdriver.ChromeOptions()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
def test_homepage_accessibility(self, driver):
"""Test homepage for WCAG violations."""
driver.get('http://localhost:3000')
axe = Axe(driver)
axe.inject()
# Run axe accessibility tests
results = axe.run()
# Assert no violations
assert len(results['violations']) == 0, \
axe.report(results['violations'])
def test_form_accessibility(self, driver):
"""Test form accessibility."""
driver.get('http://localhost:3000/contact')
axe = Axe(driver)
axe.inject()
# Run with specific tags
results = axe.run(options={
'runOnly': {
'type': 'tag',
'values': ['wcag2a', 'wcag2aa', 'wcag21aa']
}
})
violations = results['violations']
assert len(violations) == 0, \
f"Found {len(violations)} accessibility violations"
def test_keyboard_navigation(self, driver):
"""Test keyboard navigation."""
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
driver.get('http://localhost:3000')
body = driver.find_element(By.TAG_NAME, 'body')
# Tab through focusable elements
for _ in range(10):
body.send_keys(Keys.TAB)
active = driver.switch_to.active_element
tag_name = active.tag_name
# Focused element should be interactive
assert tag_name in ['a', 'button', 'input', 'select', 'textarea'], \
f"Unexpected focused element: {tag_name}"#!/bin/bash
# scaffold-tests.sh - Generate test file scaffolding
# Usage: ./scaffold-tests.sh <source_file> [--framework jest|pytest|mocha]
set -euo pipefail
SOURCE_FILE="${{1:?Usage: $0 <source_file> [--framework jest|pytest|mocha]}}"
FRAMEWORK="${{2:-jest}}"
echo "Scaffolding tests for: $SOURCE_FILE (framework: $FRAMEWORK)"
# TODO: Implement test scaffolding logic
# - Parse source file for exported functions/classes
# - Generate test stubs for each export
# - Include setup/teardown boilerplate
# - Add common assertion patterns
echo "Test scaffolding complete."
// Test Template
// TODO: Customize for your testing framework and project
describe('ModuleName', () => {
// Setup
beforeEach(() => {
// TODO: Add test setup
});
afterEach(() => {
// TODO: Add cleanup
});
describe('functionName', () => {
it('should handle the happy path', () => {
// TODO: Add assertion
});
it('should handle edge cases', () => {
// TODO: Add edge case tests
});
it('should handle errors gracefully', () => {
// TODO: Add error handling tests
});
});
});
Related skills
How it compares
Pick accessibility-testing for guided a11y QA plans rather than performance profiling or generic unit test generation skills.
FAQ
What checks does accessibility-testing include?
accessibility-testing covers WCAG audits, keyboard navigation checks, screen reader flows, and remediation planning for web and mobile surfaces before release.
When should developers invoke accessibility-testing?
accessibility-testing fits pre-release QA when UI needs WCAG review, keyboard paths are untested, or screen reader flows must be validated before shipping inclusive interfaces.