Accessibility A11y Enhanced
Scannednpx machina-cli add skill PramodDutta/qaskills/accessibility-a11y-enhanced --openclawAccessibility A11y Enhanced Skill
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
Setting Up Automated Testing
With Playwright and axe-core
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});
npm install --save-dev @axe-core/playwright
// tests/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility tests', () => {
test('should not have any automatically detectable accessibility issues', async ({ page }) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should have accessible homepage', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.exclude('#third-party-widget') // Exclude third-party content
.analyze();
// Log violations for debugging
if (results.violations.length > 0) {
console.log('Accessibility violations:', JSON.stringify(results.violations, null, 2));
}
expect(results.violations).toEqual([]);
});
test('should have accessible forms', async ({ page }) => {
await page.goto('/contact');
const results = await new AxeBuilder({ page })
.include('form') // Test only forms
.analyze();
expect(results.violations).toEqual([]);
});
test('should meet specific WCAG rules', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withRules(['color-contrast', 'image-alt', 'label', 'aria-required-attr'])
.analyze();
expect(results.violations).toEqual([]);
});
});
With Cypress and axe-core
// cypress/support/commands.ts
import 'cypress-axe';
Cypress.Commands.add('checkA11y', (context?: string, options?: any) => {
cy.injectAxe();
cy.checkA11y(context, options, (violations) => {
if (violations.length) {
cy.task('log', violations);
}
});
});
// cypress/e2e/accessibility.cy.ts
describe('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 element
await page.keyboard.press('Tab');
const firstFocusedElement = await page.evaluate(() => document.activeElement?.tagName);
expect(['A', 'BUTTON', 'INPUT']).toContain(firstFocusedElement);
// Tab through all interactive elements
for (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 Enter
await page.focus('button[type="submit"]');
await page.keyboard.press('Enter');
await expect(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"]');
await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.getByRole('dialog')).not.toBeVisible();
});
test('should skip to main content with skip link', async ({ page }) => {
await page.goto('/');
// Tab to skip link
await page.keyboard.press('Tab');
const skipLink = page.getByText('Skip to main content');
await expect(skipLink).toBeFocused();
// Activate skip link
await page.keyboard.press('Enter');
// Main content should now be focused
const mainContent = page.locator('main');
await expect(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 styles
const 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 focus
expect(
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');
await expect(modal).toBeVisible();
// Tab through modal elements
await page.keyboard.press('Tab');
const firstFocusable = await page.evaluate(() => document.activeElement?.id);
// Keep tabbing until we cycle back
for (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 body
const 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-label
const nav = page.locator('nav');
const ariaLabel = await nav.getAttribute('aria-label');
expect(ariaLabel).toBeTruthy();
// Check buttons have accessible names
const 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 as HTMLElement).ariaLabel ||
(el as HTMLElement).innerText ||
(el as HTMLElement).title
);
expect(accessibleName).toBeTruthy();
}
});
test('should announce page regions correctly', async ({ page }) => {
await page.goto('/');
// Check for landmark regions
const 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 images
expect(alt !== null || role === 'presentation').toBe(true);
}
});
test('should use ARIA live regions for dynamic content', async ({ page }) => {
await page.goto('/notifications');
// Trigger a notification
await page.click('button[aria-label="Show notification"]');
const liveRegion = page.locator('[aria-live="polite"]');
await expect(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 = await new AxeBuilder({ 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 visible
const heading = page.getByRole('heading', { level: 1 });
await expect(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 label
const 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 fields
await page.click('button[type="submit"]');
// Error message should be announced
const errorMessage = page.locator('[role="alert"]');
await expect(errorMessage).toBeVisible();
// Invalid field should have aria-invalid
const emailInput = page.locator('#email');
const ariaInvalid = await emailInput.getAttribute('aria-invalid');
expect(ariaInvalid).toBe('true');
// Error should be associated with field
const 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 -->
<button aria-label="Close dialog">Ć</button>
<!-- Good: Button with text content -->
<button>Submit</button>
<!-- Bad: No accessible name -->
<button><span class="icon-close"></span></button>
2. Dialog/Modal Pattern
<!-- Modal with proper ARIA -->
<div
role="dialog"
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-modal="true"
>
<h2 id="modal-title">Confirm Action</h2>
<p id="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');
await expect(tablist).toBeVisible();
// Individual tabs should have role="tab"
const firstTab = page.getByRole('tab', { name: 'Tab 1' });
await expect(firstTab).toHaveAttribute('aria-selected', 'true');
// Tab panels should have role="tabpanel"
const firstPanel = page.getByRole('tabpanel', { name: 'Tab 1' });
await expect(firstPanel).toBeVisible();
// Arrow keys should navigate tabs
await firstTab.focus();
await page.keyboard.press('ArrowRight');
const secondTab = page.getByRole('tab', { name: 'Tab 2' });
await expect(secondTab).toBeFocused();
await expect(secondTab).toHaveAttribute('aria-selected', 'true');
});
4. Combobox/Autocomplete Pattern
test('should implement accessible autocomplete', async ({ page }) => {
await page.goto('/search');
const combobox = page.getByRole('combobox');
await expect(combobox).toHaveAttribute('aria-expanded', 'false');
// Type to trigger autocomplete
await combobox.fill('test');
await expect(combobox).toHaveAttribute('aria-expanded', 'true');
// Listbox should appear
const listbox = page.getByRole('listbox');
await expect(listbox).toBeVisible();
// Navigate with arrow keys
await page.keyboard.press('ArrowDown');
const firstOption = page.getByRole('option').first();
await expect(firstOption).toHaveAttribute('aria-selected', 'true');
});
Accessibility Testing Checklist
Page Level
- Page has a unique, descriptive title
- Page language is declared (
<html lang="en">) - Heading hierarchy is logical (h1 ā h2 ā h3)
- Skip to main content link is provided
- Landmark regions are properly defined
Images
- All images have appropriate alt text
- Decorative images use
alt=""orrole="presentation" - Complex images have extended descriptions
- Icons have accessible labels
Forms
- All form controls have labels
- Required fields are indicated
- Error messages are clear and accessible
- Field validation is announced
- Form can be completed with keyboard only
Interactive Elements
- All interactive elements are keyboard accessible
- Focus order is logical
- Focus indicators are visible
- Modals trap focus correctly
- ARIA roles are used correctly
Color and Contrast
- Color contrast meets WCAG AA (4.5:1 for text)
- Information not conveyed by color alone
- High contrast mode is supported
Dynamic Content
- ARIA live regions announce changes
- Loading states are announced
- Dynamic content updates are perceivable
Best Practices
- Use semantic HTML --
<button>over<div role="button">. - Provide text alternatives -- Alt text, captions, transcripts.
- Ensure keyboard access -- All functionality must work without mouse.
- Test with real screen readers -- NVDA, JAWS, VoiceOver.
- Use ARIA sparingly -- Only when HTML semantics are insufficient.
- Maintain focus order -- Logical tab order matches visual order.
- Announce state changes -- Use ARIA live regions for dynamic updates.
- Test with zoom -- Content should be usable at 200% zoom.
- Support high contrast -- Respect user color preferences.
- Document accessibility features -- Help users discover a11y features.
Anti-Patterns to Avoid
- Using
role="button"on<div>-- Use native<button>. - Missing alt text on images -- Always provide alternative text.
- Keyboard traps -- Users must be able to navigate away.
- Invisible focus indicators --
outline: nonewithout replacement. - ARIA overuse -- Don't reinvent native semantics.
- Color-only information -- Use text labels or patterns too.
- Auto-playing media -- Provide controls, allow user to pause.
- Time limits without extension -- Allow users to extend time.
- Inconsistent navigation -- Keep navigation predictable.
- Relying on automated tests alone -- Manual testing is essential.
Tools and Resources
Automated Testing:
- axe-core (Playwright, Cypress integration)
- Lighthouse (Chrome DevTools)
- WAVE browser extension
Manual Testing:
- NVDA (Windows screen reader)
- JAWS (Windows screen reader)
- VoiceOver (macOS/iOS screen reader)
- Keyboard navigation
- Color contrast analyzers
Learning Resources:
- WCAG 2.1 Guidelines
- ARIA Authoring Practices Guide
- WebAIM articles and resources
Accessibility is not optional. Building inclusive experiences makes the web better for everyone.
Source
git clone https://github.com/PramodDutta/qaskills/blob/main/seed-skills/accessibility-a11y-enhanced/SKILL.mdView on GitHub Overview
Accessibility A11y Enhanced delivers end-to-end WCAG compliance checks for ARIA, keyboard navigation, screen readers, and color contrast. It provides automated a11y validation using axe-core with Playwright and Cypress, helping teams catch regressions early and ship inclusive web apps.
How This Skill Works
The skill integrates axe-core into Playwright and Cypress test pipelines to scan pages against WCAG 2.1 rules (e.g., color-contrast, image-alt, label). Tests can be configured with include/exclude filters, and violations are surfaced for remediation, aligning with POUR principles and WCAG levels.
When to Use It
- During initial accessibility audits of new or redesigned web apps
- Integrating automated a11y checks into CI/CD to prevent regressions
- Testing forms, ARIA attributes, and keyboard navigation flows
- Verifying color contrast and skip links across themes and states
- Isolating accessibility tests for third-party widgets or embedded content
Quick Start
- Step 1: Install dependencies for Playwright/Cypress + axe-core (e.g., @axe-core/playwright) and configure your test runner
- Step 2: Add Axe-based tests (using AxeBuilder for Playwright or cy.checkA11y for Cypress) to validate WCAG rules like color-contrast, image-alt, and label
- Step 3: Run the tests locally or in CI, review violations, and implement fixes (alt text, labels, focus management, contrast improvements)
Best Practices
- Run WCAG 2.1 A/AA checks (and consider AAA where feasible) with axe-core rules like color-contrast, image-alt, label, and aria-required-attr
- Use Playwright + Axe or Cypress + Axe with precise selectors to focus coverage on critical pages
- Exclude known noisy third-party widgets to reduce false positives while testing core flows
- Fix violations iteratively by improving markup: add descriptive alt text, proper labels, and accessible names
- Integrate results into CI so builds fail on new accessibility violations and trigger remediation workflows
Example Use Cases
- Homepage accessibility scan using axe-core with Playwright to ensure zero violations
- Form validation tests verifying labels, aria-required-attr, and error messaging
- Color-contrast checks across light/dark themes ensuring AA level compliance
- Cypress-based a11y suite with include/exclude rules and violation logging
- CI integration that blocks PRs on new accessibility violations and reports findings
Frequently Asked Questions
Related Skills
Accessibility Auditor
PramodDutta/qaskills
Comprehensive WCAG 2.1 AA compliance testing combining automated axe-core scans with manual keyboard navigation, screen reader compatibility, and focus management verification
Website Audit
KaliBellion/qaskills
Comprehensive website auditing skill using Lighthouse, PageSpeed Insights, and web performance APIs to audit performance, accessibility, SEO, best practices, and security.
Axe-core Accessibility Testing
KaliBellion/qaskills
Accessibility testing skill using axe-core and Playwright for automated WCAG 2.1 compliance auditing, custom rules, and accessibility reporting.
axe-core Accessibility Automation
PramodDutta/qaskills
Automated accessibility testing with axe-core integrated into CI pipelines, including custom rule configuration, issue prioritization, and remediation guidance.