A comprehensive React + Vite application demonstrating enterprise-level web accessibility (a11y) implementation, built as a learning resource for understanding WCAG 2.1 AA accessibility standards.
This project provides a practical implementation of web accessibility concepts. Each component, pattern, and test demonstrates specific accessibility techniques and best practices that can be applied to real-world applications.
- Semantic HTML Structure: Proper use of landmarks (nav, main, footer, etc.)
- Accessible Forms: Proper label associations, error handling, and ARIA attributes
- Focus Management: Client-side routing focus handling and modal focus trapping
- Screen Reader Support: Optimized for NVDA, VoiceOver, and other assistive technologies
- Automated Testing: ESLint, jest-axe, and cypress-axe integration
- Documentation: Comprehensive learning notes and implementation guides
- Node.js 18+ and npm
- A modern browser (Chrome, Firefox, Safari, or Edge)
- (Optional) Screen reader for testing (NVDA for Windows, VoiceOver for macOS)
# Install dependencies
npm install
# Start development server
npm run dev
# Open browser to http://localhost:5173npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Run ESLint with accessibility checks
npm run test # Run Jest unit tests
npm run test:e2e # Open Cypress for E2E testing
npm run test:e2e:headless # Run Cypress tests in headless modeweb-a11y/
├── docs/ # Learning documentation
│ ├── strategy-and-semantics.md
│ ├── aria-and-screen-readers.md
│ ├── focus-management.md
│ ├── testing-and-processes.md
│ ├── accessibility-checklist.md
│ └── definition-of-done.md
├── src/
│ ├── components/ # Reusable components
│ │ ├── Modal.tsx # Accessible modal with focus trap
│ │ └── Modal.test.tsx
│ ├── pages/ # Page components
│ │ ├── HomePage.tsx
│ │ ├── FormsPage.tsx # Form accessibility demo
│ │ └── ModalsPage.tsx # Modal focus management demo
│ ├── App.tsx # Main app with routing
│ ├── index.css # Global styles with a11y utilities
│ └── setupTests.ts # Jest configuration
├── cypress/
│ ├── e2e/
│ │ └── accessibility.cy.ts # E2E accessibility tests
│ └── support/
│ └── commands.ts # Custom Cypress commands
├── .eslintrc.cjs # ESLint with jsx-a11y plugin
├── jest.config.js # Jest configuration
├── cypress.config.ts # Cypress configuration
└── package.json
Topics covered:
- Business case for accessibility (legal, market reach, SEO)
- Shift-left methodology
- Semantic HTML vs generic divs
- Native button elements
.sr-onlyutility class for screen reader-only content
Implementation:
src/App.tsx:30-60- Semantic landmarkssrc/index.css:27-37-.sr-onlyclasssrc/pages/FormsPage.tsx:130-140- Native buttons
Topics covered:
- Accessible name computation hierarchy
- Accessibility tree structure
- Screen reader interaction modes (Browse/Focus)
- Native label elements vs span labels
Implementation:
src/pages/FormsPage.tsx:60-110- Proper label associationssrc/pages/FormsPage.tsx:72- ARIA attributes
Topics covered:
- Client-side routing focus management
- Modal focus trap implementation
:focus-visiblefor keyboard-only focus indicators- Framework-specific accessibility considerations
Implementation:
src/App.tsx:17-40- Route change focus managementsrc/components/Modal.tsx:15-100- Focus trapsrc/index.css:40-60- Focus indicators
Topics covered:
- ESLint with jsx-a11y plugin
- Unit testing with jest-axe
- E2E testing with cypress-axe
- Accessibility personas and Definition of Done
Implementation:
.eslintrc.cjs- Linting configurationsrc/components/Modal.test.tsx- Unit testscypress/e2e/accessibility.cy.ts- E2E testsdocs/definition-of-done.md- Process integration
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}useEffect(() => {
const heading = document.querySelector('h1')
if (heading) {
heading.setAttribute('tabindex', '-1')
heading.focus()
heading.addEventListener('blur', () => {
heading.removeAttribute('tabindex')
}, { once: true })
}
}, [location.pathname])// Trap Tab key within modal
const focusableElements = modalRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusableElements[0]
const last = focusableElements[focusableElements.length - 1]
if (e.shiftKey && document.activeElement === first) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault()
first.focus()
}Linting (ESLint + jsx-a11y):
npm run lintCatches: Missing alt text, invalid ARIA, unlabeled inputs, div buttons
Unit Tests (Jest + jest-axe):
npm run testCatches: Component-level ARIA issues, missing labels, incorrect roles
E2E Tests (Cypress + cypress-axe):
npm run test:e2eCatches: Page-level issues, color contrast, focus management
Keyboard Navigation:
- Press Tab to navigate through interactive elements
- Verify all elements are reachable
- Check focus indicators are visible
- Test modals trap focus correctly
Screen Reader Testing:
- Windows: NVDA + Chrome/Firefox
- macOS: VoiceOver + Safari
- iOS: VoiceOver + Safari
Before considering a feature complete:
- Linting passes (no jsx-a11y warnings)
- Unit tests pass with jest-axe
- E2E tests pass with cypress-axe
- Manual keyboard testing completed
- Screen reader tested
- Color contrast verified (4.5:1 for text)
- Focus indicators visible
- All images have alt text
- All form inputs have labels
- Error messages announced
See docs/accessibility-checklist.md for complete checklist.
Tested with:
- ✅ Chrome + NVDA (Windows)
- ✅ Firefox + NVDA (Windows)
- ✅ Safari + VoiceOver (macOS)
- ✅ Safari + VoiceOver (iOS)
- ✅ Edge + Narrator (Windows)
- Div Buttons: Use
<button>instead of<div onClick={...}> - Span Labels: Use
<label htmlFor="id">instead of<span> - Placeholder as Label: Always provide a proper label
- display:none on SR content: Use
.sr-onlypattern instead - Auto-focus without user action: Only focus on user interaction
- Color-only information: Provide text or icon indicators too
- Positive tabindex: Use
tabindex="-1"or"0"only
- axe DevTools - Browser extension
- WAVE - Web accessibility evaluator
- WebAIM Contrast Checker
This is a learning project demonstrating accessibility best practices. If you find accessibility issues or improvements:
- Check the accessibility checklist
- Run automated tests (
npm run lint,npm run test,npm run test:e2e) - Test with keyboard and screen reader
- Submit an issue or pull request
This project is created for educational purposes.
- Enterprise Accessibility curriculum
- WCAG 2.1 Working Group
- axe-core and Deque Systems
- React Testing Library and Cypress teams
- WebAIM and The A11Y Project
Built with accessibility in mind from day one.