Frontend Accessibility Overview
Build accessible web applications following WCAG guidelines with semantic HTML, ARIA attributes, keyboard navigation, and screen reader support for inclusive user experiences.
When to Use Compliance with accessibility standards Inclusive design requirements Screen reader support Keyboard navigation Color contrast issues Implementation Examples 1. Semantic HTML and ARIA
Article Title
Article content...
Confirm Action
Are you sure?
- Keyboard Navigation // React Component with keyboard support import React, { useEffect, useRef, useState } from 'react';
interface MenuItem { id: string; label: string; href: string; }
const KeyboardNavigationMenu: React.FC<{ items: MenuItem[] }> = ({ items }) => {
const [activeIndex, setActiveIndex] = useState(0);
const menuRef = useRef
useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowLeft': case 'ArrowUp': e.preventDefault(); setActiveIndex(prev => prev === 0 ? items.length - 1 : prev - 1 ); break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
setActiveIndex(prev =>
prev === items.length - 1 ? 0 : prev + 1
);
break;
case 'Home':
e.preventDefault();
setActiveIndex(0);
break;
case 'End':
e.preventDefault();
setActiveIndex(items.length - 1);
break;
case 'Enter':
case ' ':
e.preventDefault();
const link = menuRef.current?.querySelectorAll('a')[activeIndex];
link?.click();
break;
case 'Escape':
menuRef.current?.querySelector('a')?.blur();
break;
default:
break;
}
};
menuRef.current?.addEventListener('keydown', handleKeyDown);
return () => menuRef.current?.removeEventListener('keydown', handleKeyDown);
}, [items.length, activeIndex]);
return (
- Color Contrast and Visual Accessibility / Proper color contrast (WCAG AA: 4.5:1 for text, 3:1 for large text) / :root { --color-text: #1a1a1a; / Black - high contrast / --color-background: #ffffff; --color-primary: #0066cc; / Blue with good contrast / --color-success: #008000; / Not pure green / --color-error: #d32f2f; / Not pure red / --color-warning: #ff8c00; / Not yellow / }
body { color: var(--color-text); background-color: var(--color-background); font-size: 16px; line-height: 1.5; }
a { color: var(--color-primary); text-decoration: underline; / Don't rely on color alone / }
button { min-height: 44px; / Touch target size / min-width: 44px; padding: 10px 20px; border-radius: 4px; font-size: 16px; cursor: pointer; }
/ Focus visible for keyboard navigation / button:focus-visible, a:focus-visible, input:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 2px; }
/ High contrast mode support / @media (prefers-contrast: more) { body { font-weight: 500; }
button { border: 2px solid currentColor; } }
/ Reduced motion support / @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } }
/ Dark mode support / @media (prefers-color-scheme: dark) { :root { --color-text: #e0e0e0; --color-background: #1a1a1a; --color-primary: #6495ed; } }
- Screen Reader Announcements // LiveRegion component for announcements interface LiveRegionProps { message: string; politeness?: 'polite' | 'assertive' | 'off'; role?: 'status' | 'alert'; }
const LiveRegion: React.FC
useEffect(() => { if (message && ref.current) { ref.current.textContent = message; } }, [message]);
return (
); };// Usage in component const SearchResults: React.FC = () => { const [results, setResults] = useState([]); const [message, setMessage] = useState('');
const handleSearch = async (query: string) => {
const response = await fetch(/api/search?q=${query});
const data = await response.json();
setResults(data);
setMessage(Found ${data.length} results);
};
return (
<>
-
{results.map(item => (
- {item.title} ))}
// Skip to main content link (hidden by default) const skipLink = document.createElement('a'); skipLink.href = '#main-content'; skipLink.textContent = 'Skip to main content'; skipLink.style.position = 'absolute'; skipLink.style.top = '-40px'; skipLink.style.left = '0'; skipLink.style.background = '#000'; skipLink.style.color = '#fff'; skipLink.style.padding = '8px'; skipLink.style.zIndex = '100'; skipLink.addEventListener('focus', () => { skipLink.style.top = '0'; }); skipLink.addEventListener('blur', () => { skipLink.style.top = '-40px'; }); document.body.insertBefore(skipLink, document.body.firstChild);
- Accessibility Testing // jest-axe integration test import { render } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import { Button } from './Button';
expect.extend(toHaveNoViolations);
describe('Button Accessibility', () => { it('should not have accessibility violations', async () => { const { container } = render( );
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should have proper ARIA labels', async () => { const { container } = render( );
const results = await axe(container);
expect(results).toHaveNoViolations();
}); });
// Accessibility Checker Hook const useAccessibilityChecker = () => { useEffect(() => { // Run accessibility checks in development if (process.env.NODE_ENV === 'development') { import('axe-core').then(axe => { axe.run((error, results) => { if (results.violations.length > 0) { console.warn('Accessibility violations found:', results.violations); } }); }); } }, []); };
Best Practices Use semantic HTML elements Provide meaningful alt text for images Ensure 4.5:1 color contrast ratio for text Support keyboard navigation entirely Use ARIA only when necessary Test with screen readers (NVDA, JAWS) Implement skip links Support zoom up to 200% Use descriptive link text Test with actual assistive technologies Resources WCAG 2.1 Guidelines WAI-ARIA Authoring Practices MDN Accessibility Axe DevTools WAVE Browser Extension WebAIM Resources