In this essential lesson, you'll learn how to build accessible React applications using proven design patterns. You'll understand the fundamental principles of web accessibility, implement ARIA attributes correctly, master keyboard navigation, and apply common UI patterns that enhance user experience for everyone, including users with disabilities.
Web accessibility means designing websites and applications that can be used by everyone, regardless of their abilities or disabilities. The term "a11y" is a numeronym for "accessibility" - representing the 11 letters between 'a' and 'y'.
Why Accessibility Matters:
WCAG Guidelines: The Web Content Accessibility Guidelines (WCAG) provide four main principles:
Semantic HTML provides meaning to web content, which is crucial for accessibility:
Use Semantic Elements: Choose HTML elements based on their meaning, not appearance:
// Good - semantic
<article>
<header>
<h2>Article Title</h2>
</header>
<main>
<p>Article content...</p>
</main>
</article>
// Avoid - non-semantic
<div>
<div className="title">Article Title</div>
<div className="content">Article content...</div>
</div>
Form Labels: Always associate form controls with labels:
<label htmlFor="email">Email Address</label>
<input id="email" type="email" required />
// Or with implicit labeling
<label>
Email Address:
<input type="email" required />
</label>
Button vs. Div: Use proper interactive elements:
// Good - semantic button
<button onClick={handleClick}>Submit</button>
// Avoid - non-interactive div
<div onClick={handleClick}>Submit</div>
ARIA (Accessible Rich Internet Applications) attributes enhance accessibility when semantic HTML isn't sufficient:
ARIA Roles: Define the purpose of elements:
<div role="navigation">
<nav>Navigation content</nav>
</div>
<div role="main">
<main>Main content</main>
</div>
<button aria-expanded={isOpen} aria-controls="menu">
Menu
</button>
<div id="menu" hidden={!isOpen}>
Menu items
</div>
ARIA Properties: Provide additional information about elements:
<input
type="text"
aria-label="Search products"
aria-describedby="search-help"
/>
<div id="search-help">Enter product name or category</div>
<button aria-describedby="delete-warning">
Delete
</button>
<div id="delete-warning">This action cannot be undone</div>
ARIA States: Indicate current state of elements:
<button aria-pressed={isActive}>
Toggle Button
</button>
<div aria-live="polite" aria-atomic="true">
{notification && <p>{notification}</p>}
</div>
Tab Panels: Implement accessible tab interfaces:
<div role="tablist">
<button
role="tab"
aria-selected={activeTab === 'profile'}
aria-controls="profile-panel"
onClick={() => setActiveTab('profile')}
>
Profile
</button>
<button
role="tab"
aria-selected={activeTab === 'settings'}
aria-controls="settings-panel"
onClick={() => setActiveTab('settings')}
>
Settings
</button>
</div>
<div
id="profile-panel"
role="tabpanel"
hidden={activeTab !== 'profile'}
>
Profile content
</div>
<div
id="settings-panel"
role="tabpanel"
hidden={activeTab !== 'settings'}
>
Settings content
</div>
Modal Dialogs: Create accessible modals:
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirmation</h2>
<p id="dialog-description">Are you sure you want to delete?</p>
<button onClick={onConfirm}>Confirm</button>
<button onClick={onCancel}>Cancel</button>
</div>
Proper focus management is essential for keyboard accessibility:
Tab Order: Ensure logical tab order through interactive elements:
// Good - natural tab order
<form>
<label htmlFor="name">Name</label>
<input id="name" />
<label htmlFor="email">Email</label>
<input id="email" />
<button type="submit">Submit</button>
</form>
Focus Trapping: Trap focus within modals and dropdowns:
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef(null)
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus()
// Trap focus logic here
}
}, [isOpen])
if (!isOpen) return null
return (
<div ref={modalRef} tabIndex={-1}>
{children}
<button onClick={onClose}>Close</button>
</div>
)
}
Skip Links: Provide skip navigation for keyboard users:
<a href="#main-content" className="skip-link">
Skip to main content
</a>
<main id="main-content">
Main content
</main>
Handle keyboard interactions properly:
function Button({ onClick, children, ...props }) {
const handleKeyDown = (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
onClick(event)
}
}
return (
<button
onClick={onClick}
onKeyDown={handleKeyDown}
{...props}
>
{children}
</button>
)
}
// Custom dropdown with keyboard navigation
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
const [focusedIndex, setFocusedIndex] = useState(-1)
const handleKeyDown = (event) => {
if (!isOpen) return
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
setFocusedIndex(prev => Math.min(prev + 1, items.length - 1))
break
case 'ArrowUp':
event.preventDefault()
setFocusedIndex(prev => Math.max(prev - 1, 0))
break
case 'Escape':
setIsOpen(false)
break
}
}
return (
<div onKeyDown={handleKeyDown}>
{/* Dropdown content */}
</div>
)
}
The Provider pattern manages and shares state across components without prop drilling:
Context Provider: Create shared state management:
const ThemeContext = createContext()
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
export function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
Multiple Providers: Combine different context providers:
export function AppProviders({ children }) {
return (
<ThemeProvider>
<AuthProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</AuthProvider>
</ThemeProvider>
)
}
Compound components manage related state and behavior across multiple components:
Tab System: Create compound tab components:
const TabContext = createContext()
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabContext.Provider>
)
}
function TabList({ children }) {
return <div role="tablist">{children}</div>
}
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabContext)
const isActive = activeTab === value
return (
<button
role="tab"
aria-selected={isActive}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabContext)
const isActive = activeTab === value
return isActive ? <div role="tabpanel">{children}</div> : null
}
Usage:
<Tabs defaultValue="profile">
<TabList>
<Tab value="profile">Profile</Tab>
<Tab value="settings">Settings</Tab>
</TabList>
<TabPanel value="profile">Profile content</TabPanel>
<TabPanel value="settings">Settings content</TabPanel>
</Tabs>
Render props provide flexibility by accepting functions as children:
Data Fetching Component:
function DataFetcher({ url, children }) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
.catch(err => {
setError(err)
setLoading(false)
})
}, [url])
return children({ data, loading, error })
}
// Usage
<DataFetcher url="/api/users">
{({ data, loading, error }) => {
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return <div>{data.map(user => <UserCard key={user.id} user={user} />)}</div>
}}
</DataFetcher>
Mouse Tracking Component:
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return children(position)
}
// Usage
<MouseTracker>
{({ x, y }) => (
<div>Mouse position: {x}, {y}</div>
)}
</MouseTracker>
Create accessible accordions with proper ARIA attributes:
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState(null)
return (
<div>
{items.map((item, index) => (
<div key={index}>
<button
aria-expanded={openIndex === index}
aria-controls={`panel-${index}`}
onClick={() => setOpenIndex(openIndex === index ? null : index)}
>
{item.title}
</button>
<div
id={`panel-${index}`}
role="region"
hidden={openIndex !== index}
>
{item.content}
</div>
</div>
))}
</div>
)
}
Build accessible navigation with keyboard support:
function NavigationMenu({ items }) {
const [focusedIndex, setFocusedIndex] = useState(-1)
const handleKeyDown = (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
setFocusedIndex(prev => (prev + 1) % items.length)
break
case 'ArrowUp':
event.preventDefault()
setFocusedIndex(prev => (prev - 1 + items.length) % items.length)
break
case 'Home':
event.preventDefault()
setFocusedIndex(0)
break
case 'End':
event.preventDefault()
setFocusedIndex(items.length - 1)
break
}
}
return (
<nav role="navigation" aria-label="Main navigation">
<ul onKeyDown={handleKeyDown}>
{items.map((item, index) => (
<li key={item.href}>
<a
href={item.href}
aria-current={item.active ? 'page' : undefined}
ref={index === focusedIndex ? el => el?.focus() : null}
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
)
}
Implement accessible form validation:
function FormField({ label, error, ...props }) {
const id = useId()
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? `${id}-error` : undefined}
{...props}
/>
{error && (
<div id={`${id}-error`} role="alert">
{error}
</div>
)}
</div>
)
}
Use tools to test accessibility automatically:
React Testing Library: Test accessibility with jest-dom matchers:
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
test('button is accessible', () => {
render(<button>Submit</button>)
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit')
})
test('form has proper labels', () => {
render(
<form>
<label htmlFor="email">Email</label>
<input id="email" type="email" />
</form>
)
expect(screen.getByLabelText('Email')).toBeInTheDocument()
})
Axe-core Integration: Automated accessibility testing:
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
test('component is accessible', async () => {
const { container } = render(<MyComponent />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Perform manual accessibility testing:
Keyboard Navigation: Test all functionality with keyboard only:
Screen Reader Testing: Test with screen readers:
Color Contrast: Verify sufficient contrast ratios:
Follow these accessibility best practices:
Semantic HTML First: Use appropriate HTML elements for their semantic meaning
<button> for actions, <a> for navigation<h1>-<h6>) for document structure<ul>, <ol>, <li>) for list contentProvide Alternatives: Ensure content is accessible in multiple ways:
Test Early and Often: Integrate accessibility testing throughout development:
Color-Only Information: Don't rely solely on color to convey information:
// Bad - color only
<span className="text-red">Error</span>
<span className="text-green">Success</span>
// Good - includes text/icon
<span className="text-red">❌ Error</span>
<span className="text-green">✅ Success</span>
Auto-Playing Content: Avoid auto-playing media or provide controls:
// Good - user-controlled media
<video controls>
<source src="video.mp4" type="video/mp4" />
</video>
Small Touch Targets: Ensure adequate touch target sizes (44x44px minimum):
// Good - sufficient touch target
<button className="min-h-[44px] min-w-[44px] p-2">
<Icon className="h-4 w-4" />
</button>
In this lesson, you've learned the essential principles of accessibility and common React design patterns. You now understand:
In the next module, you'll learn about data fetching and state management, building on these accessibility and design pattern foundations to create robust, user-friendly applications.