mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
feat: add Conductor plugin for Context-Driven Development
Add comprehensive Conductor plugin implementing Context-Driven Development methodology with tracks, specs, and phased implementation plans. Components: - 5 commands: setup, new-track, implement, status, revert - 1 agent: conductor-validator - 3 skills: context-driven-development, track-management, workflow-patterns - 18 templates for project artifacts Documentation updates: - README.md: Updated counts (68 plugins, 100 agents, 110 skills, 76 tools) - docs/plugins.md: Added Conductor to Workflows section - docs/agents.md: Added conductor-validator agent - docs/agent-skills.md: Added Conductor skills section Also includes Prettier formatting across all project files.
This commit is contained in:
@@ -7,9 +7,11 @@ model: sonnet
|
||||
You are an experienced UI visual validation expert specializing in comprehensive visual testing and design verification through rigorous analysis methodologies.
|
||||
|
||||
## Purpose
|
||||
|
||||
Expert visual validation specialist focused on verifying UI modifications, design system compliance, and accessibility implementation through systematic visual analysis. Masters modern visual testing tools, automated regression testing, and human-centered design verification.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Default assumption: The modification goal has NOT been achieved until proven otherwise
|
||||
- Be highly critical and look for flaws, inconsistencies, or incomplete implementations
|
||||
- Ignore any code hints or implementation details - base judgments solely on visual evidence
|
||||
@@ -19,6 +21,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
## Capabilities
|
||||
|
||||
### Visual Analysis Mastery
|
||||
|
||||
- Screenshot analysis with pixel-perfect precision
|
||||
- Visual diff detection and change identification
|
||||
- Cross-browser and cross-device visual consistency verification
|
||||
@@ -29,6 +32,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Accessibility visual compliance assessment
|
||||
|
||||
### Modern Visual Testing Tools
|
||||
|
||||
- **Chromatic**: Visual regression testing for Storybook components
|
||||
- **Percy**: Cross-browser visual testing and screenshot comparison
|
||||
- **Applitools**: AI-powered visual testing and validation
|
||||
@@ -39,6 +43,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- **Storybook Visual Testing**: Isolated component validation
|
||||
|
||||
### Design System Validation
|
||||
|
||||
- Component library compliance verification
|
||||
- Design token implementation accuracy
|
||||
- Brand consistency and style guide adherence
|
||||
@@ -49,6 +54,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Multi-brand design system validation
|
||||
|
||||
### Accessibility Visual Verification
|
||||
|
||||
- WCAG 2.1/2.2 visual compliance assessment
|
||||
- Color contrast ratio validation and measurement
|
||||
- Focus indicator visibility and design verification
|
||||
@@ -59,6 +65,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Screen reader compatible design verification
|
||||
|
||||
### Cross-Platform Visual Consistency
|
||||
|
||||
- Responsive design breakpoint validation
|
||||
- Mobile-first design implementation verification
|
||||
- Native app vs web consistency checking
|
||||
@@ -69,6 +76,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Platform-specific design guideline compliance
|
||||
|
||||
### Automated Visual Testing Integration
|
||||
|
||||
- CI/CD pipeline visual testing integration
|
||||
- GitHub Actions automated screenshot comparison
|
||||
- Visual regression testing in pull request workflows
|
||||
@@ -79,6 +87,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Automated design token compliance checking
|
||||
|
||||
### Manual Visual Inspection Techniques
|
||||
|
||||
- Systematic visual audit methodologies
|
||||
- Edge case and boundary condition identification
|
||||
- User flow visual consistency verification
|
||||
@@ -89,6 +98,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Progressive disclosure and information architecture validation
|
||||
|
||||
### Visual Quality Assurance
|
||||
|
||||
- Pixel-perfect implementation verification
|
||||
- Image optimization and visual quality assessment
|
||||
- Typography rendering and font loading validation
|
||||
@@ -99,6 +109,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Cross-team design implementation consistency
|
||||
|
||||
## Analysis Process
|
||||
|
||||
1. **Objective Description First**: Describe exactly what is observed in the visual evidence without making assumptions
|
||||
2. **Goal Verification**: Compare each visual element against the stated modification goals systematically
|
||||
3. **Measurement Validation**: For changes involving rotation, position, size, or alignment, verify through visual measurement
|
||||
@@ -109,6 +120,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
8. **Edge Case Analysis**: Examine edge cases, error states, and boundary conditions
|
||||
|
||||
## Mandatory Verification Checklist
|
||||
|
||||
- [ ] Have I described the actual visual content objectively?
|
||||
- [ ] Have I avoided inferring effects from code changes?
|
||||
- [ ] For rotations: Have I confirmed aspect ratio changes?
|
||||
@@ -124,6 +136,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- [ ] Have I questioned whether 'different' equals 'correct'?
|
||||
|
||||
## Advanced Validation Techniques
|
||||
|
||||
- **Pixel Diff Analysis**: Precise change detection through pixel-level comparison
|
||||
- **Layout Shift Detection**: Cumulative Layout Shift (CLS) visual assessment
|
||||
- **Animation Frame Analysis**: Frame-by-frame animation validation
|
||||
@@ -134,6 +147,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- **Print Preview Validation**: Print stylesheet and layout verification
|
||||
|
||||
## Output Requirements
|
||||
|
||||
- Start with 'From the visual evidence, I observe...'
|
||||
- Provide detailed visual measurements when relevant
|
||||
- Clearly state whether goals are achieved, partially achieved, or not achieved
|
||||
@@ -144,6 +158,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Document edge cases and boundary conditions observed
|
||||
|
||||
## Behavioral Traits
|
||||
|
||||
- Maintains skeptical approach until visual proof is provided
|
||||
- Applies systematic methodology to all visual assessments
|
||||
- Considers accessibility and inclusive design in every evaluation
|
||||
@@ -154,6 +169,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Advocates for comprehensive visual quality assurance practices
|
||||
|
||||
## Forbidden Behaviors
|
||||
|
||||
- Assuming code changes automatically produce visual results
|
||||
- Quick conclusions without thorough systematic analysis
|
||||
- Accepting 'looks different' as 'looks correct'
|
||||
@@ -163,6 +179,7 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- Making assumptions about user behavior from visual evidence alone
|
||||
|
||||
## Example Interactions
|
||||
|
||||
- "Validate that the new button component meets accessibility contrast requirements"
|
||||
- "Verify that the responsive navigation collapses correctly at mobile breakpoints"
|
||||
- "Confirm that the loading spinner animation displays smoothly across browsers"
|
||||
@@ -172,4 +189,4 @@ Expert visual validation specialist focused on verifying UI modifications, desig
|
||||
- "Confirm that form validation states provide clear visual feedback"
|
||||
- "Assess whether the data table maintains readability across different screen sizes"
|
||||
|
||||
Your role is to be the final gatekeeper ensuring UI modifications actually work as intended through uncompromising visual verification with accessibility and inclusive design considerations at the forefront.
|
||||
Your role is to be the final gatekeeper ensuring UI modifications actually work as intended through uncompromising visual verification with accessibility and inclusive design considerations at the forefront.
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
You are an accessibility expert specializing in WCAG compliance, inclusive design, and assistive technology compatibility. Conduct comprehensive audits, identify barriers, provide remediation guidance, and ensure digital products are accessible to all users.
|
||||
|
||||
## Context
|
||||
|
||||
The user needs to audit and improve accessibility to ensure compliance with WCAG standards and provide an inclusive experience for users with disabilities. Focus on automated testing, manual verification, remediation strategies, and establishing ongoing accessibility practices.
|
||||
|
||||
## Requirements
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Instructions
|
||||
@@ -14,69 +16,69 @@ $ARGUMENTS
|
||||
|
||||
```javascript
|
||||
// accessibility-test.js
|
||||
const { AxePuppeteer } = require('@axe-core/puppeteer');
|
||||
const puppeteer = require('puppeteer');
|
||||
const { AxePuppeteer } = require("@axe-core/puppeteer");
|
||||
const puppeteer = require("puppeteer");
|
||||
|
||||
class AccessibilityAuditor {
|
||||
constructor(options = {}) {
|
||||
this.wcagLevel = options.wcagLevel || 'AA';
|
||||
this.viewport = options.viewport || { width: 1920, height: 1080 };
|
||||
}
|
||||
constructor(options = {}) {
|
||||
this.wcagLevel = options.wcagLevel || "AA";
|
||||
this.viewport = options.viewport || { width: 1920, height: 1080 };
|
||||
}
|
||||
|
||||
async runFullAudit(url) {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport(this.viewport);
|
||||
await page.goto(url, { waitUntil: 'networkidle2' });
|
||||
async runFullAudit(url) {
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport(this.viewport);
|
||||
await page.goto(url, { waitUntil: "networkidle2" });
|
||||
|
||||
const results = await new AxePuppeteer(page)
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.exclude('.no-a11y-check')
|
||||
.analyze();
|
||||
const results = await new AxePuppeteer(page)
|
||||
.withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"])
|
||||
.exclude(".no-a11y-check")
|
||||
.analyze();
|
||||
|
||||
await browser.close();
|
||||
await browser.close();
|
||||
|
||||
return {
|
||||
url,
|
||||
timestamp: new Date().toISOString(),
|
||||
violations: results.violations.map(v => ({
|
||||
id: v.id,
|
||||
impact: v.impact,
|
||||
description: v.description,
|
||||
help: v.help,
|
||||
helpUrl: v.helpUrl,
|
||||
nodes: v.nodes.map(n => ({
|
||||
html: n.html,
|
||||
target: n.target,
|
||||
failureSummary: n.failureSummary
|
||||
}))
|
||||
})),
|
||||
score: this.calculateScore(results)
|
||||
};
|
||||
}
|
||||
return {
|
||||
url,
|
||||
timestamp: new Date().toISOString(),
|
||||
violations: results.violations.map((v) => ({
|
||||
id: v.id,
|
||||
impact: v.impact,
|
||||
description: v.description,
|
||||
help: v.help,
|
||||
helpUrl: v.helpUrl,
|
||||
nodes: v.nodes.map((n) => ({
|
||||
html: n.html,
|
||||
target: n.target,
|
||||
failureSummary: n.failureSummary,
|
||||
})),
|
||||
})),
|
||||
score: this.calculateScore(results),
|
||||
};
|
||||
}
|
||||
|
||||
calculateScore(results) {
|
||||
const weights = { critical: 10, serious: 5, moderate: 2, minor: 1 };
|
||||
let totalWeight = 0;
|
||||
results.violations.forEach(v => {
|
||||
totalWeight += weights[v.impact] || 0;
|
||||
});
|
||||
return Math.max(0, 100 - totalWeight);
|
||||
}
|
||||
calculateScore(results) {
|
||||
const weights = { critical: 10, serious: 5, moderate: 2, minor: 1 };
|
||||
let totalWeight = 0;
|
||||
results.violations.forEach((v) => {
|
||||
totalWeight += weights[v.impact] || 0;
|
||||
});
|
||||
return Math.max(0, 100 - totalWeight);
|
||||
}
|
||||
}
|
||||
|
||||
// Component testing with jest-axe
|
||||
import { render } from '@testing-library/react';
|
||||
import { axe, toHaveNoViolations } from 'jest-axe';
|
||||
import { render } from "@testing-library/react";
|
||||
import { axe, toHaveNoViolations } from "jest-axe";
|
||||
|
||||
expect.extend(toHaveNoViolations);
|
||||
|
||||
describe('Accessibility Tests', () => {
|
||||
it('should have no violations', async () => {
|
||||
const { container } = render(<MyComponent />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
describe("Accessibility Tests", () => {
|
||||
it("should have no violations", async () => {
|
||||
const { container } = render(<MyComponent />);
|
||||
const results = await axe(container);
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -162,62 +164,67 @@ class ColorContrastAnalyzer {
|
||||
```javascript
|
||||
// keyboard-navigation.js
|
||||
class KeyboardNavigationTester {
|
||||
async testKeyboardNavigation(page) {
|
||||
const results = { focusableElements: [], missingFocusIndicators: [], keyboardTraps: [] };
|
||||
async testKeyboardNavigation(page) {
|
||||
const results = {
|
||||
focusableElements: [],
|
||||
missingFocusIndicators: [],
|
||||
keyboardTraps: [],
|
||||
};
|
||||
|
||||
// Get all focusable elements
|
||||
const focusable = await page.evaluate(() => {
|
||||
const selector = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
return Array.from(document.querySelectorAll(selector)).map(el => ({
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
text: el.innerText || el.value || el.placeholder || '',
|
||||
tabIndex: el.tabIndex
|
||||
}));
|
||||
});
|
||||
// Get all focusable elements
|
||||
const focusable = await page.evaluate(() => {
|
||||
const selector =
|
||||
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
||||
return Array.from(document.querySelectorAll(selector)).map((el) => ({
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
text: el.innerText || el.value || el.placeholder || "",
|
||||
tabIndex: el.tabIndex,
|
||||
}));
|
||||
});
|
||||
|
||||
results.focusableElements = focusable;
|
||||
results.focusableElements = focusable;
|
||||
|
||||
// Test tab order and focus indicators
|
||||
for (let i = 0; i < focusable.length; i++) {
|
||||
await page.keyboard.press('Tab');
|
||||
// Test tab order and focus indicators
|
||||
for (let i = 0; i < focusable.length; i++) {
|
||||
await page.keyboard.press("Tab");
|
||||
|
||||
const focused = await page.evaluate(() => {
|
||||
const el = document.activeElement;
|
||||
return {
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
hasFocusIndicator: window.getComputedStyle(el).outline !== 'none'
|
||||
};
|
||||
});
|
||||
const focused = await page.evaluate(() => {
|
||||
const el = document.activeElement;
|
||||
return {
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
hasFocusIndicator: window.getComputedStyle(el).outline !== "none",
|
||||
};
|
||||
});
|
||||
|
||||
if (!focused.hasFocusIndicator) {
|
||||
results.missingFocusIndicators.push(focused);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
if (!focused.hasFocusIndicator) {
|
||||
results.missingFocusIndicators.push(focused);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhance keyboard accessibility
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.querySelector('.modal.open');
|
||||
if (modal) closeModal(modal);
|
||||
}
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
const modal = document.querySelector(".modal.open");
|
||||
if (modal) closeModal(modal);
|
||||
}
|
||||
});
|
||||
|
||||
// Make div clickable accessible
|
||||
document.querySelectorAll('[onclick]').forEach(el => {
|
||||
if (!['a', 'button', 'input'].includes(el.tagName.toLowerCase())) {
|
||||
el.setAttribute('tabindex', '0');
|
||||
el.setAttribute('role', 'button');
|
||||
el.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
el.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("[onclick]").forEach((el) => {
|
||||
if (!["a", "button", "input"].includes(el.tagName.toLowerCase())) {
|
||||
el.setAttribute("tabindex", "0");
|
||||
el.setAttribute("role", "button");
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
el.click();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
@@ -226,94 +233,98 @@ document.querySelectorAll('[onclick]').forEach(el => {
|
||||
```javascript
|
||||
// screen-reader-test.js
|
||||
class ScreenReaderTester {
|
||||
async testScreenReaderCompatibility(page) {
|
||||
async testScreenReaderCompatibility(page) {
|
||||
return {
|
||||
landmarks: await this.testLandmarks(page),
|
||||
headings: await this.testHeadingStructure(page),
|
||||
images: await this.testImageAccessibility(page),
|
||||
forms: await this.testFormAccessibility(page),
|
||||
};
|
||||
}
|
||||
|
||||
async testHeadingStructure(page) {
|
||||
const headings = await page.evaluate(() => {
|
||||
return Array.from(
|
||||
document.querySelectorAll("h1, h2, h3, h4, h5, h6"),
|
||||
).map((h) => ({
|
||||
level: parseInt(h.tagName[1]),
|
||||
text: h.textContent.trim(),
|
||||
isEmpty: !h.textContent.trim(),
|
||||
}));
|
||||
});
|
||||
|
||||
const issues = [];
|
||||
let previousLevel = 0;
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
if (heading.level > previousLevel + 1 && previousLevel !== 0) {
|
||||
issues.push({
|
||||
type: "skipped-level",
|
||||
message: `Heading level ${heading.level} skips from level ${previousLevel}`,
|
||||
});
|
||||
}
|
||||
if (heading.isEmpty) {
|
||||
issues.push({ type: "empty-heading", index });
|
||||
}
|
||||
previousLevel = heading.level;
|
||||
});
|
||||
|
||||
if (!headings.some((h) => h.level === 1)) {
|
||||
issues.push({ type: "missing-h1", message: "Page missing h1 element" });
|
||||
}
|
||||
|
||||
return { headings, issues };
|
||||
}
|
||||
|
||||
async testFormAccessibility(page) {
|
||||
const forms = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll("form")).map((form) => {
|
||||
const inputs = form.querySelectorAll("input, textarea, select");
|
||||
return {
|
||||
landmarks: await this.testLandmarks(page),
|
||||
headings: await this.testHeadingStructure(page),
|
||||
images: await this.testImageAccessibility(page),
|
||||
forms: await this.testFormAccessibility(page)
|
||||
fields: Array.from(inputs).map((input) => ({
|
||||
type: input.type || input.tagName.toLowerCase(),
|
||||
id: input.id,
|
||||
hasLabel: input.id
|
||||
? !!document.querySelector(`label[for="${input.id}"]`)
|
||||
: !!input.closest("label"),
|
||||
hasAriaLabel: !!input.getAttribute("aria-label"),
|
||||
required: input.required,
|
||||
})),
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async testHeadingStructure(page) {
|
||||
const headings = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).map(h => ({
|
||||
level: parseInt(h.tagName[1]),
|
||||
text: h.textContent.trim(),
|
||||
isEmpty: !h.textContent.trim()
|
||||
}));
|
||||
});
|
||||
|
||||
const issues = [];
|
||||
let previousLevel = 0;
|
||||
|
||||
headings.forEach((heading, index) => {
|
||||
if (heading.level > previousLevel + 1 && previousLevel !== 0) {
|
||||
issues.push({
|
||||
type: 'skipped-level',
|
||||
message: `Heading level ${heading.level} skips from level ${previousLevel}`
|
||||
});
|
||||
}
|
||||
if (heading.isEmpty) {
|
||||
issues.push({ type: 'empty-heading', index });
|
||||
}
|
||||
previousLevel = heading.level;
|
||||
});
|
||||
|
||||
if (!headings.some(h => h.level === 1)) {
|
||||
issues.push({ type: 'missing-h1', message: 'Page missing h1 element' });
|
||||
const issues = [];
|
||||
forms.forEach((form, i) => {
|
||||
form.fields.forEach((field, j) => {
|
||||
if (!field.hasLabel && !field.hasAriaLabel) {
|
||||
issues.push({ type: "missing-label", form: i, field: j });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { headings, issues };
|
||||
}
|
||||
|
||||
async testFormAccessibility(page) {
|
||||
const forms = await page.evaluate(() => {
|
||||
return Array.from(document.querySelectorAll('form')).map(form => {
|
||||
const inputs = form.querySelectorAll('input, textarea, select');
|
||||
return {
|
||||
fields: Array.from(inputs).map(input => ({
|
||||
type: input.type || input.tagName.toLowerCase(),
|
||||
id: input.id,
|
||||
hasLabel: input.id ? !!document.querySelector(`label[for="${input.id}"]`) : !!input.closest('label'),
|
||||
hasAriaLabel: !!input.getAttribute('aria-label'),
|
||||
required: input.required
|
||||
}))
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const issues = [];
|
||||
forms.forEach((form, i) => {
|
||||
form.fields.forEach((field, j) => {
|
||||
if (!field.hasLabel && !field.hasAriaLabel) {
|
||||
issues.push({ type: 'missing-label', form: i, field: j });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { forms, issues };
|
||||
}
|
||||
return { forms, issues };
|
||||
}
|
||||
}
|
||||
|
||||
// ARIA patterns
|
||||
const ariaPatterns = {
|
||||
modal: `
|
||||
modal: `
|
||||
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
|
||||
<h2 id="modal-title">Modal Title</h2>
|
||||
<button aria-label="Close">×</button>
|
||||
</div>`,
|
||||
|
||||
tabs: `
|
||||
tabs: `
|
||||
<div role="tablist" aria-label="Navigation">
|
||||
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">Content</div>`,
|
||||
|
||||
form: `
|
||||
form: `
|
||||
<label for="name">Name <span aria-label="required">*</span></label>
|
||||
<input id="name" required aria-required="true" aria-describedby="name-error">
|
||||
<span id="name-error" role="alert" aria-live="polite"></span>`
|
||||
<span id="name-error" role="alert" aria-live="polite"></span>`,
|
||||
};
|
||||
```
|
||||
|
||||
@@ -323,6 +334,7 @@ const ariaPatterns = {
|
||||
## Manual Accessibility Testing
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- [ ] All interactive elements accessible via Tab
|
||||
- [ ] Buttons activate with Enter/Space
|
||||
- [ ] Esc key closes modals
|
||||
@@ -331,6 +343,7 @@ const ariaPatterns = {
|
||||
- [ ] Logical tab order
|
||||
|
||||
### Screen Reader
|
||||
|
||||
- [ ] Page title descriptive
|
||||
- [ ] Headings create logical outline
|
||||
- [ ] Images have alt text
|
||||
@@ -339,6 +352,7 @@ const ariaPatterns = {
|
||||
- [ ] Dynamic updates announced
|
||||
|
||||
### Visual
|
||||
|
||||
- [ ] Text resizes to 200% without loss
|
||||
- [ ] Color not sole means of info
|
||||
- [ ] Focus indicators have sufficient contrast
|
||||
@@ -346,6 +360,7 @@ const ariaPatterns = {
|
||||
- [ ] Animations can be paused
|
||||
|
||||
### Cognitive
|
||||
|
||||
- [ ] Instructions clear and simple
|
||||
- [ ] Error messages helpful
|
||||
- [ ] No time limits on forms
|
||||
@@ -357,29 +372,37 @@ const ariaPatterns = {
|
||||
|
||||
```javascript
|
||||
// Fix missing alt text
|
||||
document.querySelectorAll('img:not([alt])').forEach(img => {
|
||||
const isDecorative = img.role === 'presentation' || img.closest('[role="presentation"]');
|
||||
img.setAttribute('alt', isDecorative ? '' : img.title || 'Image');
|
||||
document.querySelectorAll("img:not([alt])").forEach((img) => {
|
||||
const isDecorative =
|
||||
img.role === "presentation" || img.closest('[role="presentation"]');
|
||||
img.setAttribute("alt", isDecorative ? "" : img.title || "Image");
|
||||
});
|
||||
|
||||
// Fix missing labels
|
||||
document.querySelectorAll('input:not([aria-label]):not([id])').forEach(input => {
|
||||
document
|
||||
.querySelectorAll("input:not([aria-label]):not([id])")
|
||||
.forEach((input) => {
|
||||
if (input.placeholder) {
|
||||
input.setAttribute('aria-label', input.placeholder);
|
||||
input.setAttribute("aria-label", input.placeholder);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// React accessible components
|
||||
const AccessibleButton = ({ children, onClick, ariaLabel, ...props }) => (
|
||||
<button onClick={onClick} aria-label={ariaLabel} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
<button onClick={onClick} aria-label={ariaLabel} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
const LiveRegion = ({ message, politeness = 'polite' }) => (
|
||||
<div role="status" aria-live={politeness} aria-atomic="true" className="sr-only">
|
||||
{message}
|
||||
</div>
|
||||
const LiveRegion = ({ message, politeness = "polite" }) => (
|
||||
<div
|
||||
role="status"
|
||||
aria-live={politeness}
|
||||
aria-atomic="true"
|
||||
className="sr-only"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
@@ -396,35 +419,35 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Install and build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
- name: Install and build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Start server
|
||||
run: |
|
||||
npm start &
|
||||
npx wait-on http://localhost:3000
|
||||
- name: Start server
|
||||
run: |
|
||||
npm start &
|
||||
npx wait-on http://localhost:3000
|
||||
|
||||
- name: Run axe tests
|
||||
run: npm run test:a11y
|
||||
- name: Run axe tests
|
||||
run: npm run test:a11y
|
||||
|
||||
- name: Run pa11y
|
||||
run: npx pa11y http://localhost:3000 --standard WCAG2AA --threshold 0
|
||||
- name: Run pa11y
|
||||
run: npx pa11y http://localhost:3000 --standard WCAG2AA --threshold 0
|
||||
|
||||
- name: Upload report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: a11y-report
|
||||
path: a11y-report.html
|
||||
- name: Upload report
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: a11y-report
|
||||
path: a11y-report.html
|
||||
```
|
||||
|
||||
### 8. Reporting
|
||||
@@ -432,8 +455,8 @@ jobs:
|
||||
```javascript
|
||||
// report-generator.js
|
||||
class AccessibilityReportGenerator {
|
||||
generateHTMLReport(auditResults) {
|
||||
return `
|
||||
generateHTMLReport(auditResults) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -458,17 +481,21 @@ class AccessibilityReportGenerator {
|
||||
</div>
|
||||
|
||||
<h2>Violations</h2>
|
||||
${auditResults.violations.map(v => `
|
||||
${auditResults.violations
|
||||
.map(
|
||||
(v) => `
|
||||
<div class="violation ${v.impact}">
|
||||
<h3>${v.help}</h3>
|
||||
<p><strong>Impact:</strong> ${v.impact}</p>
|
||||
<p>${v.description}</p>
|
||||
<a href="${v.helpUrl}">Learn more</a>
|
||||
</div>
|
||||
`).join('')}
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ Practical guide to testing web applications with screen readers for comprehensiv
|
||||
|
||||
### 1. Major Screen Readers
|
||||
|
||||
| Screen Reader | Platform | Browser | Usage |
|
||||
|---------------|----------|---------|-------|
|
||||
| **VoiceOver** | macOS/iOS | Safari | ~15% |
|
||||
| **NVDA** | Windows | Firefox/Chrome | ~31% |
|
||||
| **JAWS** | Windows | Chrome/IE | ~40% |
|
||||
| **TalkBack** | Android | Chrome | ~10% |
|
||||
| **Narrator** | Windows | Edge | ~4% |
|
||||
| Screen Reader | Platform | Browser | Usage |
|
||||
| ------------- | --------- | -------------- | ----- |
|
||||
| **VoiceOver** | macOS/iOS | Safari | ~15% |
|
||||
| **NVDA** | Windows | Firefox/Chrome | ~31% |
|
||||
| **JAWS** | Windows | Chrome/IE | ~40% |
|
||||
| **TalkBack** | Android | Chrome | ~10% |
|
||||
| **Narrator** | Windows | Edge | ~4% |
|
||||
|
||||
### 2. Testing Priority
|
||||
|
||||
@@ -44,11 +44,11 @@ Comprehensive Coverage:
|
||||
|
||||
### 3. Screen Reader Modes
|
||||
|
||||
| Mode | Purpose | When Used |
|
||||
|------|---------|-----------|
|
||||
| **Browse/Virtual** | Read content | Default reading |
|
||||
| **Focus/Forms** | Interact with controls | Filling forms |
|
||||
| **Application** | Custom widgets | ARIA applications |
|
||||
| Mode | Purpose | When Used |
|
||||
| ------------------ | ---------------------- | ----------------- |
|
||||
| **Browse/Virtual** | Read content | Default reading |
|
||||
| **Focus/Forms** | Interact with controls | Filling forms |
|
||||
| **Application** | Custom widgets | ARIA applications |
|
||||
|
||||
## VoiceOver (macOS)
|
||||
|
||||
@@ -101,22 +101,26 @@ VO + Cmd + T Next table
|
||||
## VoiceOver Testing Checklist
|
||||
|
||||
### Page Load
|
||||
|
||||
- [ ] Page title announced
|
||||
- [ ] Main landmark found
|
||||
- [ ] Skip link works
|
||||
|
||||
### Navigation
|
||||
|
||||
- [ ] All headings discoverable via rotor
|
||||
- [ ] Heading levels logical (H1 → H2 → H3)
|
||||
- [ ] Landmarks properly labeled
|
||||
- [ ] Skip links functional
|
||||
|
||||
### Links & Buttons
|
||||
|
||||
- [ ] Link purpose clear
|
||||
- [ ] Button actions described
|
||||
- [ ] New window/tab announced
|
||||
|
||||
### Forms
|
||||
|
||||
- [ ] All labels read with inputs
|
||||
- [ ] Required fields announced
|
||||
- [ ] Error messages read
|
||||
@@ -124,12 +128,14 @@ VO + Cmd + T Next table
|
||||
- [ ] Focus moves to errors
|
||||
|
||||
### Dynamic Content
|
||||
|
||||
- [ ] Alerts announced immediately
|
||||
- [ ] Loading states communicated
|
||||
- [ ] Content updates announced
|
||||
- [ ] Modals trap focus correctly
|
||||
|
||||
### Tables
|
||||
|
||||
- [ ] Headers associated with cells
|
||||
- [ ] Table navigation works
|
||||
- [ ] Complex tables have captions
|
||||
@@ -151,11 +157,11 @@ VO + Cmd + T Next table
|
||||
<div id="results" role="status" aria-live="polite">New results loaded</div>
|
||||
|
||||
<!-- Issue: Form error not read -->
|
||||
<input type="email">
|
||||
<input type="email" />
|
||||
<span class="error">Invalid email</span>
|
||||
|
||||
<!-- Fix -->
|
||||
<input type="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<input type="email" aria-invalid="true" aria-describedby="email-error" />
|
||||
<span id="email-error" role="alert">Invalid email</span>
|
||||
```
|
||||
|
||||
@@ -235,23 +241,27 @@ Watch for:
|
||||
## NVDA Test Script
|
||||
|
||||
### Initial Load
|
||||
|
||||
1. Navigate to page
|
||||
2. Let page finish loading
|
||||
3. Press Insert + Down to read all
|
||||
4. Note: Page title, main content identified?
|
||||
|
||||
### Landmark Navigation
|
||||
|
||||
1. Press D repeatedly
|
||||
2. Check: All main areas reachable?
|
||||
3. Check: Landmarks properly labeled?
|
||||
|
||||
### Heading Navigation
|
||||
|
||||
1. Press Insert + F7 → Headings
|
||||
2. Check: Logical heading structure?
|
||||
3. Press H to navigate headings
|
||||
4. Check: All sections discoverable?
|
||||
|
||||
### Form Testing
|
||||
|
||||
1. Press F to find first form field
|
||||
2. Check: Label read?
|
||||
3. Fill in invalid data
|
||||
@@ -260,12 +270,14 @@ Watch for:
|
||||
6. Check: Focus moved to error?
|
||||
|
||||
### Interactive Elements
|
||||
|
||||
1. Tab through all interactive elements
|
||||
2. Check: Each announces role and state
|
||||
3. Activate buttons with Enter/Space
|
||||
4. Check: Result announced?
|
||||
|
||||
### Dynamic Content
|
||||
|
||||
1. Trigger content update
|
||||
2. Check: Change announced?
|
||||
3. Open modal
|
||||
@@ -345,10 +357,12 @@ Reading Controls (swipe up then right):
|
||||
|
||||
```html
|
||||
<!-- Accessible modal structure -->
|
||||
<div role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-describedby="dialog-desc">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="dialog-title"
|
||||
aria-describedby="dialog-desc"
|
||||
>
|
||||
<h2 id="dialog-title">Confirm Delete</h2>
|
||||
<p id="dialog-desc">This action cannot be undone.</p>
|
||||
<button>Cancel</button>
|
||||
@@ -363,10 +377,10 @@ function openModal(modal) {
|
||||
lastFocus = document.activeElement;
|
||||
|
||||
// Move focus to modal
|
||||
modal.querySelector('h2').focus();
|
||||
modal.querySelector("h2").focus();
|
||||
|
||||
// Trap focus
|
||||
modal.addEventListener('keydown', trapFocus);
|
||||
modal.addEventListener("keydown", trapFocus);
|
||||
}
|
||||
|
||||
function closeModal(modal) {
|
||||
@@ -375,9 +389,9 @@ function closeModal(modal) {
|
||||
}
|
||||
|
||||
function trapFocus(e) {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.key === "Tab") {
|
||||
const focusable = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
@@ -391,7 +405,7 @@ function trapFocus(e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === "Escape") {
|
||||
closeModal(modal);
|
||||
}
|
||||
}
|
||||
@@ -411,12 +425,13 @@ function trapFocus(e) {
|
||||
</div>
|
||||
|
||||
<!-- Progress updates -->
|
||||
<div role="progressbar"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label="Upload progress">
|
||||
</div>
|
||||
<div
|
||||
role="progressbar"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-label="Upload progress"
|
||||
></div>
|
||||
|
||||
<!-- Log (additions only) -->
|
||||
<div role="log" aria-live="polite" aria-relevant="additions">
|
||||
@@ -428,53 +443,47 @@ function trapFocus(e) {
|
||||
|
||||
```html
|
||||
<div role="tablist" aria-label="Product information">
|
||||
<button role="tab"
|
||||
id="tab-1"
|
||||
aria-selected="true"
|
||||
aria-controls="panel-1">
|
||||
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
|
||||
Description
|
||||
</button>
|
||||
<button role="tab"
|
||||
id="tab-2"
|
||||
aria-selected="false"
|
||||
aria-controls="panel-2"
|
||||
tabindex="-1">
|
||||
<button
|
||||
role="tab"
|
||||
id="tab-2"
|
||||
aria-selected="false"
|
||||
aria-controls="panel-2"
|
||||
tabindex="-1"
|
||||
>
|
||||
Reviews
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel"
|
||||
id="panel-1"
|
||||
aria-labelledby="tab-1">
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
Product description content...
|
||||
</div>
|
||||
|
||||
<div role="tabpanel"
|
||||
id="panel-2"
|
||||
aria-labelledby="tab-2"
|
||||
hidden>
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
Reviews content...
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Tab keyboard navigation
|
||||
tablist.addEventListener('keydown', (e) => {
|
||||
tablist.addEventListener("keydown", (e) => {
|
||||
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
|
||||
const index = tabs.indexOf(document.activeElement);
|
||||
|
||||
let newIndex;
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case "ArrowRight":
|
||||
newIndex = (index + 1) % tabs.length;
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case "ArrowLeft":
|
||||
newIndex = (index - 1 + tabs.length) % tabs.length;
|
||||
break;
|
||||
case 'Home':
|
||||
case "Home":
|
||||
newIndex = 0;
|
||||
break;
|
||||
case 'End':
|
||||
case "End":
|
||||
newIndex = tabs.length - 1;
|
||||
break;
|
||||
default:
|
||||
@@ -494,17 +503,18 @@ tablist.addEventListener('keydown', (e) => {
|
||||
function logAccessibleName(element) {
|
||||
const computed = window.getComputedStyle(element);
|
||||
console.log({
|
||||
role: element.getAttribute('role') || element.tagName,
|
||||
name: element.getAttribute('aria-label') ||
|
||||
element.getAttribute('aria-labelledby') ||
|
||||
element.textContent,
|
||||
role: element.getAttribute("role") || element.tagName,
|
||||
name:
|
||||
element.getAttribute("aria-label") ||
|
||||
element.getAttribute("aria-labelledby") ||
|
||||
element.textContent,
|
||||
state: {
|
||||
expanded: element.getAttribute('aria-expanded'),
|
||||
selected: element.getAttribute('aria-selected'),
|
||||
checked: element.getAttribute('aria-checked'),
|
||||
disabled: element.disabled
|
||||
expanded: element.getAttribute("aria-expanded"),
|
||||
selected: element.getAttribute("aria-selected"),
|
||||
checked: element.getAttribute("aria-checked"),
|
||||
disabled: element.disabled,
|
||||
},
|
||||
visible: computed.display !== 'none' && computed.visibility !== 'hidden'
|
||||
visible: computed.display !== "none" && computed.visibility !== "hidden",
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -512,6 +522,7 @@ function logAccessibleName(element) {
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Test with actual screen readers** - Not just simulators
|
||||
- **Use semantic HTML first** - ARIA is supplemental
|
||||
- **Test in browse and focus modes** - Different experiences
|
||||
@@ -519,6 +530,7 @@ function logAccessibleName(element) {
|
||||
- **Test keyboard only first** - Foundation for SR testing
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't assume one SR is enough** - Test multiple
|
||||
- **Don't ignore mobile** - Growing user base
|
||||
- **Don't test only happy path** - Test error states
|
||||
|
||||
@@ -20,10 +20,10 @@ Comprehensive guide to auditing web content against WCAG 2.2 guidelines with act
|
||||
|
||||
### 1. WCAG Conformance Levels
|
||||
|
||||
| Level | Description | Required For |
|
||||
|-------|-------------|--------------|
|
||||
| **A** | Minimum accessibility | Legal baseline |
|
||||
| **AA** | Standard conformance | Most regulations |
|
||||
| Level | Description | Required For |
|
||||
| ------- | ---------------------- | ----------------- |
|
||||
| **A** | Minimum accessibility | Legal baseline |
|
||||
| **AA** | Standard conformance | Most regulations |
|
||||
| **AAA** | Enhanced accessibility | Specialized needs |
|
||||
|
||||
### 2. POUR Principles
|
||||
@@ -61,10 +61,11 @@ Moderate:
|
||||
|
||||
### Perceivable (Principle 1)
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
## 1.1 Text Alternatives
|
||||
|
||||
### 1.1.1 Non-text Content (Level A)
|
||||
|
||||
- [ ] All images have alt text
|
||||
- [ ] Decorative images have alt=""
|
||||
- [ ] Complex images have long descriptions
|
||||
@@ -72,33 +73,39 @@ Moderate:
|
||||
- [ ] CAPTCHAs have alternatives
|
||||
|
||||
Check:
|
||||
|
||||
```html
|
||||
<!-- Good -->
|
||||
<img src="chart.png" alt="Sales increased 25% from Q1 to Q2">
|
||||
<img src="decorative-line.png" alt="">
|
||||
<img src="chart.png" alt="Sales increased 25% from Q1 to Q2" />
|
||||
<img src="decorative-line.png" alt="" />
|
||||
|
||||
<!-- Bad -->
|
||||
<img src="chart.png">
|
||||
<img src="decorative-line.png" alt="decorative line">
|
||||
<img src="chart.png" />
|
||||
<img src="decorative-line.png" alt="decorative line" />
|
||||
```
|
||||
````
|
||||
|
||||
## 1.2 Time-based Media
|
||||
|
||||
### 1.2.1 Audio-only and Video-only (Level A)
|
||||
|
||||
- [ ] Audio has text transcript
|
||||
- [ ] Video has audio description or transcript
|
||||
|
||||
### 1.2.2 Captions (Level A)
|
||||
|
||||
- [ ] All video has synchronized captions
|
||||
- [ ] Captions are accurate and complete
|
||||
- [ ] Speaker identification included
|
||||
|
||||
### 1.2.3 Audio Description (Level A)
|
||||
|
||||
- [ ] Video has audio description for visual content
|
||||
|
||||
## 1.3 Adaptable
|
||||
|
||||
### 1.3.1 Info and Relationships (Level A)
|
||||
|
||||
- [ ] Headings use proper tags (h1-h6)
|
||||
- [ ] Lists use ul/ol/dl
|
||||
- [ ] Tables have headers
|
||||
@@ -106,38 +113,46 @@ Check:
|
||||
- [ ] ARIA landmarks present
|
||||
|
||||
Check:
|
||||
|
||||
```html
|
||||
<!-- Heading hierarchy -->
|
||||
<h1>Page Title</h1>
|
||||
<h2>Section</h2>
|
||||
<h3>Subsection</h3>
|
||||
<h2>Another Section</h2>
|
||||
<h2>Section</h2>
|
||||
<h3>Subsection</h3>
|
||||
<h2>Another Section</h2>
|
||||
|
||||
<!-- Table headers -->
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th scope="col">Name</th><th scope="col">Price</th></tr>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
```
|
||||
|
||||
### 1.3.2 Meaningful Sequence (Level A)
|
||||
|
||||
- [ ] Reading order is logical
|
||||
- [ ] CSS positioning doesn't break order
|
||||
- [ ] Focus order matches visual order
|
||||
|
||||
### 1.3.3 Sensory Characteristics (Level A)
|
||||
|
||||
- [ ] Instructions don't rely on shape/color alone
|
||||
- [ ] "Click the red button" → "Click Submit (red button)"
|
||||
|
||||
## 1.4 Distinguishable
|
||||
|
||||
### 1.4.1 Use of Color (Level A)
|
||||
|
||||
- [ ] Color is not only means of conveying info
|
||||
- [ ] Links distinguishable without color
|
||||
- [ ] Error states not color-only
|
||||
|
||||
### 1.4.3 Contrast (Minimum) (Level AA)
|
||||
|
||||
- [ ] Text: 4.5:1 contrast ratio
|
||||
- [ ] Large text (18pt+): 3:1 ratio
|
||||
- [ ] UI components: 3:1 ratio
|
||||
@@ -145,27 +160,32 @@ Check:
|
||||
Tools: WebAIM Contrast Checker, axe DevTools
|
||||
|
||||
### 1.4.4 Resize Text (Level AA)
|
||||
|
||||
- [ ] Text resizes to 200% without loss
|
||||
- [ ] No horizontal scrolling at 320px
|
||||
- [ ] Content reflows properly
|
||||
|
||||
### 1.4.10 Reflow (Level AA)
|
||||
|
||||
- [ ] Content reflows at 400% zoom
|
||||
- [ ] No two-dimensional scrolling
|
||||
- [ ] All content accessible at 320px width
|
||||
|
||||
### 1.4.11 Non-text Contrast (Level AA)
|
||||
|
||||
- [ ] UI components have 3:1 contrast
|
||||
- [ ] Focus indicators visible
|
||||
- [ ] Graphical objects distinguishable
|
||||
|
||||
### 1.4.12 Text Spacing (Level AA)
|
||||
|
||||
- [ ] No content loss with increased spacing
|
||||
- [ ] Line height 1.5x font size
|
||||
- [ ] Paragraph spacing 2x font size
|
||||
- [ ] Letter spacing 0.12x font size
|
||||
- [ ] Word spacing 0.16x font size
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
### Operable (Principle 2)
|
||||
|
||||
@@ -183,9 +203,10 @@ Check:
|
||||
// Custom button must be keyboard accessible
|
||||
<div role="button" tabindex="0"
|
||||
onkeydown="if(event.key === 'Enter' || event.key === ' ') activate()">
|
||||
```
|
||||
````
|
||||
|
||||
### 2.1.2 No Keyboard Trap (Level A)
|
||||
|
||||
- [ ] Focus can move away from all components
|
||||
- [ ] Modal dialogs trap focus correctly
|
||||
- [ ] Focus returns after modal closes
|
||||
@@ -193,11 +214,13 @@ Check:
|
||||
## 2.2 Enough Time
|
||||
|
||||
### 2.2.1 Timing Adjustable (Level A)
|
||||
|
||||
- [ ] Session timeouts can be extended
|
||||
- [ ] User warned before timeout
|
||||
- [ ] Option to disable auto-refresh
|
||||
|
||||
### 2.2.2 Pause, Stop, Hide (Level A)
|
||||
|
||||
- [ ] Moving content can be paused
|
||||
- [ ] Auto-updating content can be paused
|
||||
- [ ] Animations respect prefers-reduced-motion
|
||||
@@ -214,12 +237,14 @@ Check:
|
||||
## 2.3 Seizures and Physical Reactions
|
||||
|
||||
### 2.3.1 Three Flashes (Level A)
|
||||
|
||||
- [ ] No content flashes more than 3 times/second
|
||||
- [ ] Flashing area is small (<25% viewport)
|
||||
|
||||
## 2.4 Navigable
|
||||
|
||||
### 2.4.1 Bypass Blocks (Level A)
|
||||
|
||||
- [ ] Skip to main content link present
|
||||
- [ ] Landmark regions defined
|
||||
- [ ] Proper heading structure
|
||||
@@ -230,14 +255,17 @@ Check:
|
||||
```
|
||||
|
||||
### 2.4.2 Page Titled (Level A)
|
||||
|
||||
- [ ] Unique, descriptive page titles
|
||||
- [ ] Title reflects page content
|
||||
|
||||
### 2.4.3 Focus Order (Level A)
|
||||
|
||||
- [ ] Focus order matches visual order
|
||||
- [ ] tabindex used correctly
|
||||
|
||||
### 2.4.4 Link Purpose (In Context) (Level A)
|
||||
|
||||
- [ ] Links make sense out of context
|
||||
- [ ] No "click here" or "read more" alone
|
||||
|
||||
@@ -250,10 +278,12 @@ Check:
|
||||
```
|
||||
|
||||
### 2.4.6 Headings and Labels (Level AA)
|
||||
|
||||
- [ ] Headings describe content
|
||||
- [ ] Labels describe purpose
|
||||
|
||||
### 2.4.7 Focus Visible (Level AA)
|
||||
|
||||
- [ ] Focus indicator visible on all elements
|
||||
- [ ] Custom focus styles meet contrast
|
||||
|
||||
@@ -265,9 +295,11 @@ Check:
|
||||
```
|
||||
|
||||
### 2.4.11 Focus Not Obscured (Level AA) - WCAG 2.2
|
||||
|
||||
- [ ] Focused element not fully hidden
|
||||
- [ ] Sticky headers don't obscure focus
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
### Understandable (Principle 3)
|
||||
|
||||
@@ -280,10 +312,12 @@ Check:
|
||||
|
||||
```html
|
||||
<html lang="en">
|
||||
```
|
||||
````
|
||||
|
||||
### 3.1.2 Language of Parts (Level AA)
|
||||
|
||||
- [ ] Language changes marked
|
||||
|
||||
```html
|
||||
<p>The French word <span lang="fr">bonjour</span> means hello.</p>
|
||||
```
|
||||
@@ -291,47 +325,56 @@ Check:
|
||||
## 3.2 Predictable
|
||||
|
||||
### 3.2.1 On Focus (Level A)
|
||||
|
||||
- [ ] No context change on focus alone
|
||||
- [ ] No unexpected popups on focus
|
||||
|
||||
### 3.2.2 On Input (Level A)
|
||||
|
||||
- [ ] No automatic form submission
|
||||
- [ ] User warned before context change
|
||||
|
||||
### 3.2.3 Consistent Navigation (Level AA)
|
||||
|
||||
- [ ] Navigation consistent across pages
|
||||
- [ ] Repeated components same order
|
||||
|
||||
### 3.2.4 Consistent Identification (Level AA)
|
||||
|
||||
- [ ] Same functionality = same label
|
||||
- [ ] Icons used consistently
|
||||
|
||||
## 3.3 Input Assistance
|
||||
|
||||
### 3.3.1 Error Identification (Level A)
|
||||
|
||||
- [ ] Errors clearly identified
|
||||
- [ ] Error message describes problem
|
||||
- [ ] Error linked to field
|
||||
|
||||
```html
|
||||
<input aria-describedby="email-error" aria-invalid="true">
|
||||
<input aria-describedby="email-error" aria-invalid="true" />
|
||||
<span id="email-error" role="alert">Please enter valid email</span>
|
||||
```
|
||||
|
||||
### 3.3.2 Labels or Instructions (Level A)
|
||||
|
||||
- [ ] All inputs have visible labels
|
||||
- [ ] Required fields indicated
|
||||
- [ ] Format hints provided
|
||||
|
||||
### 3.3.3 Error Suggestion (Level AA)
|
||||
|
||||
- [ ] Errors include correction suggestion
|
||||
- [ ] Suggestions are specific
|
||||
|
||||
### 3.3.4 Error Prevention (Level AA)
|
||||
|
||||
- [ ] Legal/financial forms reversible
|
||||
- [ ] Data checked before submission
|
||||
- [ ] User can review before submit
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
### Robust (Principle 4)
|
||||
|
||||
@@ -356,23 +399,21 @@ Check:
|
||||
aria-labelledby="label">
|
||||
</div>
|
||||
<span id="label">Accept terms</span>
|
||||
```
|
||||
````
|
||||
|
||||
### 4.1.3 Status Messages (Level AA)
|
||||
|
||||
- [ ] Status updates announced
|
||||
- [ ] Live regions used correctly
|
||||
|
||||
```html
|
||||
<div role="status" aria-live="polite">
|
||||
3 items added to cart
|
||||
</div>
|
||||
<div role="status" aria-live="polite">3 items added to cart</div>
|
||||
|
||||
<div role="alert" aria-live="assertive">
|
||||
Error: Form submission failed
|
||||
</div>
|
||||
```
|
||||
<div role="alert" aria-live="assertive">Error: Form submission failed</div>
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
## Automated Testing
|
||||
|
||||
```javascript
|
||||
@@ -405,7 +446,7 @@ test('should have no accessibility violations', async ({ page }) => {
|
||||
|
||||
expect(results.violations).toHaveLength(0);
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
```bash
|
||||
# CLI tools
|
||||
@@ -420,28 +461,32 @@ lighthouse https://example.com --only-categories=accessibility
|
||||
|
||||
```html
|
||||
<!-- Before -->
|
||||
<input type="email" placeholder="Email">
|
||||
<input type="email" placeholder="Email" />
|
||||
|
||||
<!-- After: Option 1 - Visible label -->
|
||||
<label for="email">Email address</label>
|
||||
<input id="email" type="email">
|
||||
<input id="email" type="email" />
|
||||
|
||||
<!-- After: Option 2 - aria-label -->
|
||||
<input type="email" aria-label="Email address">
|
||||
<input type="email" aria-label="Email address" />
|
||||
|
||||
<!-- After: Option 3 - aria-labelledby -->
|
||||
<span id="email-label">Email</span>
|
||||
<input type="email" aria-labelledby="email-label">
|
||||
<input type="email" aria-labelledby="email-label" />
|
||||
```
|
||||
|
||||
### Fix: Insufficient Color Contrast
|
||||
|
||||
```css
|
||||
/* Before: 2.5:1 contrast */
|
||||
.text { color: #767676; }
|
||||
.text {
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
/* After: 4.5:1 contrast */
|
||||
.text { color: #595959; }
|
||||
.text {
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
/* Or add background */
|
||||
.text {
|
||||
@@ -456,25 +501,25 @@ lighthouse https://example.com --only-categories=accessibility
|
||||
// Make custom element keyboard accessible
|
||||
class AccessibleDropdown extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.setAttribute('tabindex', '0');
|
||||
this.setAttribute('role', 'combobox');
|
||||
this.setAttribute('aria-expanded', 'false');
|
||||
this.setAttribute("tabindex", "0");
|
||||
this.setAttribute("role", "combobox");
|
||||
this.setAttribute("aria-expanded", "false");
|
||||
|
||||
this.addEventListener('keydown', (e) => {
|
||||
this.addEventListener("keydown", (e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
case "Enter":
|
||||
case " ":
|
||||
this.toggle();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
this.close();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case "ArrowDown":
|
||||
this.focusNext();
|
||||
e.preventDefault();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case "ArrowUp":
|
||||
this.focusPrevious();
|
||||
e.preventDefault();
|
||||
break;
|
||||
@@ -487,6 +532,7 @@ class AccessibleDropdown extends HTMLElement {
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Start early** - Accessibility from design phase
|
||||
- **Test with real users** - Disabled users provide best feedback
|
||||
- **Automate what you can** - 30-50% issues detectable
|
||||
@@ -494,6 +540,7 @@ class AccessibleDropdown extends HTMLElement {
|
||||
- **Document patterns** - Build accessible component library
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't rely only on automated testing** - Manual testing required
|
||||
- **Don't use ARIA as first solution** - Native HTML first
|
||||
- **Don't hide focus outlines** - Keyboard users need them
|
||||
|
||||
Reference in New Issue
Block a user