Accessibility ensures everyone can use your application, including people with disabilities. It's also a legal requirement in many jurisdictions. Here's how to test and improve accessibility.
Core Principles (WCAG)#
POUR Principles:
Perceivable
- Text alternatives for images
- Captions for video
- Sufficient color contrast
Operable
- Keyboard accessible
- Enough time to read
- No seizure-inducing content
Understandable
- Readable text
- Predictable navigation
- Input assistance
Robust
- Compatible with assistive tech
- Valid HTML
- Future-proof
Automated Testing#
Axe DevTools#
1// axe-core in tests
2import { axe, toHaveNoViolations } from 'jest-axe';
3
4expect.extend(toHaveNoViolations);
5
6describe('Button', () => {
7 it('has no accessibility violations', async () => {
8 const { container } = render(<Button>Click me</Button>);
9 const results = await axe(container);
10 expect(results).toHaveNoViolations();
11 });
12});Lighthouse CI#
1# GitHub Actions
2- name: Lighthouse CI
3 uses: treosh/lighthouse-ci-action@v10
4 with:
5 urls: |
6 https://example.com/
7 https://example.com/products
8 budgetPath: ./lighthouse-budget.json
9 uploadArtifacts: true
10
11# lighthouse-budget.json
12{
13 "assertions": {
14 "categories:accessibility": ["error", { "minScore": 0.9 }]
15 }
16}ESLint Plugin#
1// .eslintrc.js
2module.exports = {
3 plugins: ['jsx-a11y'],
4 extends: ['plugin:jsx-a11y/recommended'],
5 rules: {
6 'jsx-a11y/alt-text': 'error',
7 'jsx-a11y/anchor-has-content': 'error',
8 'jsx-a11y/click-events-have-key-events': 'error',
9 'jsx-a11y/no-static-element-interactions': 'error',
10 },
11};Manual Testing#
Keyboard Navigation#
Test all functionality with keyboard only:
Tab Move to next focusable element
Shift+Tab Move to previous element
Enter/Space Activate buttons, links
Arrow keys Navigate menus, sliders
Escape Close modals, cancel
Check for:
- Visible focus indicators
- Logical tab order
- No keyboard traps
- Skip links for navigation
Screen Reader Testing#
Popular screen readers:
- VoiceOver (Mac): Cmd+F5 to enable
- NVDA (Windows): Free download
- JAWS (Windows): Commercial
Test scenarios:
- Navigate by headings (H key)
- Navigate by landmarks (D key)
- Read page content
- Fill out forms
- Interact with custom components
Common Issues and Fixes#
Images#
1<!-- ❌ Missing alt text -->
2<img src="product.jpg">
3
4<!-- ✅ Descriptive alt text -->
5<img src="product.jpg" alt="Red wireless headphones with noise cancellation">
6
7<!-- ✅ Decorative image (empty alt) -->
8<img src="decorative-line.svg" alt="">
9
10<!-- ✅ Complex images -->
11<figure>
12 <img src="chart.png" alt="Sales increased 50% in Q4">
13 <figcaption>Quarterly sales chart showing growth trajectory</figcaption>
14</figure>Forms#
1<!-- ❌ No label association -->
2<input type="email" placeholder="Email">
3
4<!-- ✅ Proper label -->
5<label for="email">Email address</label>
6<input type="email" id="email" required aria-describedby="email-hint">
7<span id="email-hint">We'll never share your email</span>
8
9<!-- ✅ Error messages -->
10<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
11<span id="email-error" role="alert">Please enter a valid email address</span>Buttons and Links#
1<!-- ❌ Non-descriptive -->
2<a href="/products">Click here</a>
3<button>Submit</button>
4
5<!-- ✅ Descriptive -->
6<a href="/products">View all products</a>
7<button>Submit order</button>
8
9<!-- ✅ Icon buttons with labels -->
10<button aria-label="Close dialog">
11 <svg aria-hidden="true">...</svg>
12</button>Color Contrast#
1/* WCAG AA requirements:
2 Normal text: 4.5:1 contrast ratio
3 Large text (18px+): 3:1 contrast ratio
4*/
5
6/* ❌ Poor contrast */
7.text {
8 color: #999;
9 background: #fff;
10}
11
12/* ✅ Good contrast */
13.text {
14 color: #595959; /* 7:1 ratio */
15 background: #fff;
16}
17
18/* Check with tools:
19 - WebAIM Contrast Checker
20 - Chrome DevTools color picker
21*/Focus Indicators#
1/* ❌ Removing focus outline */
2*:focus {
3 outline: none;
4}
5
6/* ✅ Custom but visible focus */
7:focus-visible {
8 outline: 2px solid #4f46e5;
9 outline-offset: 2px;
10}
11
12/* ✅ Button focus */
13.button:focus-visible {
14 outline: 2px solid currentColor;
15 outline-offset: 2px;
16 box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.2);
17}ARIA Usage#
1<!-- Use native HTML when possible -->
2<!-- ❌ Over-engineering -->
3<div role="button" tabindex="0" onclick="submit()">Submit</div>
4
5<!-- ✅ Native element -->
6<button type="submit">Submit</button>
7
8<!-- ARIA for custom components -->
9<div
10 role="tablist"
11 aria-label="Product information tabs"
12>
13 <button role="tab" aria-selected="true" aria-controls="panel-1">Description</button>
14 <button role="tab" aria-selected="false" aria-controls="panel-2">Reviews</button>
15</div>
16
17<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
18 Content...
19</div>Live Regions#
1<!-- Announce dynamic updates -->
2<div aria-live="polite" aria-atomic="true">
3 <!-- Screen reader announces when content changes -->
4 Item added to cart
5</div>
6
7<!-- Urgent announcements -->
8<div role="alert" aria-live="assertive">
9 Error: Please fix the highlighted fields
10</div>Testing Checklist#
1## Automated Tests
2- [ ] axe-core passes
3- [ ] Lighthouse accessibility > 90%
4- [ ] ESLint jsx-a11y passes
5
6## Keyboard
7- [ ] All interactive elements focusable
8- [ ] Focus order logical
9- [ ] Focus indicators visible
10- [ ] No keyboard traps
11- [ ] Skip link present
12
13## Screen Reader
14- [ ] Heading hierarchy correct
15- [ ] Landmarks used properly
16- [ ] Images have alt text
17- [ ] Forms labeled correctly
18- [ ] Dynamic content announced
19
20## Visual
21- [ ] Color contrast sufficient
22- [ ] Not relying on color alone
23- [ ] Text resizable to 200%
24- [ ] Works at 400% zoomConclusion#
Accessibility is an ongoing practice, not a checkbox. Start with automated testing, complement with manual testing, and involve users with disabilities when possible.
Building accessible applications benefits everyone—and it's the right thing to do.