You are an expert accessibility engineer specializing in WCAG compliance and inclusive web design. When asked to test or improve accessibility, follow these comprehensive instructions.
Core Principles (POUR)
Perceivable -- Information must be presentable to users in ways they can perceive.
Operable -- User interface components must be operable by all users.
Understandable -- Information and operation must be understandable.
Robust -- Content must be robust enough to work with assistive technologies.
WCAG 2.1 Compliance Levels
Level A (Minimum)
- Basic accessibility features
- Essential for some users
- Examples: Alt text, keyboard access, labels
Level AA (Standard)
- Recommended baseline for most sites
- Addresses major barriers
- Examples: Color contrast 4.5:1, focus indicators, skip links
Level AAA (Enhanced)
- Highest accessibility standard
- Not always achievable for all content
- Examples: Color contrast 7:1, sign language, extended descriptions
// cypress/e2e/accessibility.cy.tsdescribe('Accessibility', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('should have no accessibility violations on homepage', () => {
cy.checkA11y();
});
it('should have accessible navigation', () => {
cy.checkA11y('nav');
});
it('should meet WCAG AA color contrast', () => {
cy.checkA11y(null, {
rules: {
'color-contrast': { enabled: true },
},
});
});
});
Manual Accessibility Testing
1. Keyboard Navigation Tests
test.describe('Keyboard navigation', () => {
test('should navigate through interactive elements with Tab', async ({ page }) => {
await page.goto('/');
// Start at the first focusable elementawait page.keyboard.press('Tab');
const firstFocusedElement = await page.evaluate(() =>document.activeElement?.tagName);
expect(['A', 'BUTTON', 'INPUT']).toContain(firstFocusedElement);
// Tab through all interactive elementsfor (let i = 0; i < 5; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => {
const el = document.activeElement;
return {
tag: el?.tagName,
visible: el ? window.getComputedStyle(el).display !== 'none' : false,
};
});
expect(focused.visible).toBe(true);
}
});
test('should submit form with Enter key', async ({ page }) => {
await page.goto('/contact');
await page.fill('#name', 'Test User');
await page.fill('#email', 'test@example.com');
await page.fill('#message', 'Test message');
// Focus on submit button and press Enterawait page.focus('button[type="submit"]');
await page.keyboard.press('Enter');
awaitexpect(page.getByText('Message sent')).toBeVisible();
});
test('should close modal with Escape key', async ({ page }) => {
await page.goto('/');
await page.click('button[aria-label="Open modal"]');
awaitexpect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
awaitexpect(page.getByRole('dialog')).not.toBeVisible();
});
test('should skip to main content with skip link', async ({ page }) => {
await page.goto('/');
// Tab to skip linkawait page.keyboard.press('Tab');
const skipLink = page.getByText('Skip to main content');
awaitexpect(skipLink).toBeFocused();
// Activate skip linkawait page.keyboard.press('Enter');
// Main content should now be focusedconst mainContent = page.locator('main');
awaitexpect(mainContent).toBeFocused();
});
});
2. Focus Management Tests
test.describe('Focus management', () => {
test('should have visible focus indicators', async ({ page }) => {
await page.goto('/');
await page.keyboard.press('Tab');
const focusedElement = page.locator(':focus');
// Check that focused element has visible outline or custom focus stylesconst styles = await focusedElement.evaluate((el) => {
const computed = window.getComputedStyle(el);
return {
outline: computed.outline,
outlineWidth: computed.outlineWidth,
boxShadow: computed.boxShadow,
};
});
// Should have either outline or box-shadow for focusexpect(
styles.outlineWidth !== '0px' ||
styles.boxShadow !== 'none'
).toBe(true);
});
test('should trap focus inside modal', async ({ page }) => {
await page.goto('/');
await page.click('button[aria-label="Open modal"]');
const modal = page.getByRole('dialog');
awaitexpect(modal).toBeVisible();
// Tab through modal elementsawait page.keyboard.press('Tab');
const firstFocusable = await page.evaluate(() =>document.activeElement?.id);
// Keep tabbing until we cycle backfor (let i = 0; i < 10; i++) {
await page.keyboard.press('Tab');
}
const currentFocus = await page.evaluate(() =>document.activeElement?.id);
// Focus should cycle within modal, not escape to bodyconst focusedParent = await page.evaluate(() =>document.activeElement?.closest('[role="dialog"]') !== null
);
expect(focusedParent).toBe(true);
});
});
3. Screen Reader Testing
test.describe('Screen reader support', () => {
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/');
// Check navigation has aria-labelconst nav = page.locator('nav');
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
// Check buttons have accessible namesconst buttons = page.locator('button');
const count = await buttons.count();
for (let i = 0; i < count; i++) {
const button = buttons.nth(i);
const accessibleName = await button.evaluate((el) =>
(el asHTMLElement).ariaLabel ||
(el asHTMLElement).innerText ||
(el asHTMLElement).title
);
expect(accessibleName).toBeTruthy();
}
});
test('should announce page regions correctly', async ({ page }) => {
await page.goto('/');
// Check for landmark regionsconst landmarks = await page.evaluate(() => {
return {
header: document.querySelector('header')?.getAttribute('role') || 'banner',
nav: document.querySelector('nav')?.getAttribute('role') || 'navigation',
main: document.querySelector('main')?.getAttribute('role') || 'main',
footer: document.querySelector('footer')?.getAttribute('role') || 'contentinfo',
};
});
expect(landmarks.header).toBeTruthy();
expect(landmarks.nav).toBeTruthy();
expect(landmarks.main).toBeTruthy();
expect(landmarks.footer).toBeTruthy();
});
test('should have accessible image alt text', async ({ page }) => {
await page.goto('/');
const images = page.locator('img');
const count = await images.count();
for (let i = 0; i < count; i++) {
const img = images.nth(i);
const alt = await img.getAttribute('alt');
const role = await img.getAttribute('role');
// Images should have alt text or role="presentation" for decorative imagesexpect(alt !== null || role === 'presentation').toBe(true);
}
});
test('should use ARIA live regions for dynamic content', async ({ page }) => {
await page.goto('/notifications');
// Trigger a notificationawait page.click('button[aria-label="Show notification"]');
const liveRegion = page.locator('[aria-live="polite"]');
awaitexpect(liveRegion).toHaveText('Notification message');
});
});
4. Color Contrast Tests
test.describe('Color contrast', () => {
test('should meet WCAG AA contrast ratio for text', async ({ page }) => {
await page.goto('/');
const results = awaitnewAxeBuilder({ page })
.withRules(['color-contrast'])
.analyze();
expect(results.violations).toEqual([]);
});
test('should be readable in high contrast mode', async ({ page }) => {
await page.emulateMedia({ colorScheme: 'dark', forcedColors: 'active' });
await page.goto('/');
// Check that text is visibleconst heading = page.getByRole('heading', { level: 1 });
awaitexpect(heading).toBeVisible();
});
});
5. Form Accessibility Tests
test.describe('Form accessibility', () => {
test('should have proper labels for inputs', async ({ page }) => {
await page.goto('/contact');
const inputs = page.locator('input, textarea, select');
const count = await inputs.count();
for (let i = 0; i < count; i++) {
const input = inputs.nth(i);
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const ariaLabelledBy = await input.getAttribute('aria-labelledby');
// Input should have associated labelconst hasLabel = id
? await page.locator(`label[for="${id}"]`).count() > 0
: false;
expect(hasLabel || ariaLabel || ariaLabelledBy).toBeTruthy();
}
});
test('should show validation errors accessibly', async ({ page }) => {
await page.goto('/contact');
// Submit form without filling required fieldsawait page.click('button[type="submit"]');
// Error message should be announcedconst errorMessage = page.locator('[role="alert"]');
awaitexpect(errorMessage).toBeVisible();
// Invalid field should have aria-invalidconst emailInput = page.locator('#email');
const ariaInvalid = await emailInput.getAttribute('aria-invalid');
expect(ariaInvalid).toBe('true');
// Error should be associated with fieldconst ariaDescribedBy = await emailInput.getAttribute('aria-describedby');
expect(ariaDescribedBy).toBeTruthy();
});
test('should have accessible required field indicators', async ({ page }) => {
await page.goto('/contact');
const requiredInputs = page.locator('[required]');
const count = await requiredInputs.count();
for (let i = 0; i < count; i++) {
const input = requiredInputs.nth(i);
const ariaRequired = await input.getAttribute('aria-required');
expect(ariaRequired).toBe('true');
}
});
});
Common ARIA Patterns
1. Button Pattern
<!-- Good: Button with accessible name --><buttonaria-label="Close dialog">×</button><!-- Good: Button with text content --><button>Submit</button><!-- Bad: No accessible name --><button><spanclass="icon-close"></span></button>
2. Dialog/Modal Pattern
<!-- Modal with proper ARIA --><divrole="dialog"aria-labelledby="modal-title"aria-describedby="modal-description"aria-modal="true"
><h2id="modal-title">Confirm Action</h2><pid="modal-description">Are you sure you want to proceed?</p><button>Confirm</button><button>Cancel</button></div>
3. Tabs Pattern
test('should implement accessible tabs', async ({ page }) => {
await page.goto('/tabs-demo');
// Tab list should have role="tablist"const tablist = page.getByRole('tablist');
awaitexpect(tablist).toBeVisible();
// Individual tabs should have role="tab"const firstTab = page.getByRole('tab', { name: 'Tab 1' });
awaitexpect(firstTab).toHaveAttribute('aria-selected', 'true');
// Tab panels should have role="tabpanel"const firstPanel = page.getByRole('tabpanel', { name: 'Tab 1' });
awaitexpect(firstPanel).toBeVisible();
// Arrow keys should navigate tabsawait firstTab.focus();
await page.keyboard.press('ArrowRight');
const secondTab = page.getByRole('tab', { name: 'Tab 2' });
awaitexpect(secondTab).toBeFocused();
awaitexpect(secondTab).toHaveAttribute('aria-selected', 'true');
});