Lead
Accessibility isn't an afterthought—it's a fundamental part of good web development. Yet most developers struggle with implementing ARIA patterns correctly and creating components that work seamlessly with assistive technologies. Here's how to build web components that are truly accessible from the ground up.
Problem
Modern web applications rely heavily on custom components, but most fail basic accessibility tests:
- Keyboard navigation breaks when focus management isn't handled properly
- Screen readers can't understand complex interactive elements without proper ARIA labels
- Color-only indicators exclude users with visual impairments
- Dynamic content updates go unnoticed by assistive technologies
- Form validation provides visual feedback but no programmatic announcements
The result? Millions of users can't effectively use your application, and you're potentially violating accessibility laws.
Approach
Instead of retrofitting accessibility, I build it into components from the start using a systematic approach:
- Semantic HTML first - Start with the right foundation
- Progressive enhancement - Add interactivity without breaking core functionality
- ARIA patterns - Follow established patterns for complex widgets
- Testing with real users - Validate with actual assistive technology users
Steps
1. Start with Semantic HTML
Every accessible component begins with proper semantic markup:
<!-- Bad: Div soup with no meaning -->
<div class="button" onclick="doSomething()">
Click me
</div>
<!-- Good: Semantic button element -->
<button type="button" onclick="doSomething()">
Click me
</button>
Always ask: "What HTML element naturally represents this functionality?" Start there, then enhance.
2. Implement Proper Focus Management
Interactive components must handle keyboard navigation correctly:
// components/accessible-modal.tsx
import { useEffect, useRef } from 'react'
export function AccessibleModal({ isOpen, onClose, children }) {
const dialogRef = useRef<HTMLDialogElement>(null)
const previousFocus = useRef<Element | null>(null)
useEffect(() => {
if (isOpen) {
// Store the previously focused element
previousFocus.current = document.activeElement
// Focus the modal
dialogRef.current?.focus()
// Trap focus within modal
const trapFocus = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
const focusableElements = dialogRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
// Focus trapping logic here
}
}
document.addEventListener('keydown', trapFocus)
return () => document.removeEventListener('keydown', trapFocus)
} else {
// Return focus to previous element
if (previousFocus.current instanceof HTMLElement) {
previousFocus.current.focus()
}
}
}, [isOpen])
if (!isOpen) return null
return (
<dialog
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal"
>
<div className="modal-content">
{children}
<button onClick={onClose} aria-label="Close modal">
×
</button>
</div>
</dialog>
)
}
3. Add ARIA Labels and Descriptions
Provide context for screen reader users:
// components/accessible-form.tsx
export function AccessibleForm() {
const [errors, setErrors] = useState<Record<string, string>>({})
return (
<form>
<div className="field-group">
<label htmlFor="email" className="required">
Email Address
</label>
<input
id="email"
type="email"
aria-required="true"
aria-describedby={errors.email ? "email-error email-hint" : "email-hint"}
aria-invalid={!!errors.email}
/>
<div id="email-hint" className="hint">
We'll never share your email with anyone else.
</div>
{errors.email && (
<div id="email-error" className="error" role="alert">
{errors.email}
</div>
)}
</div>
</form>
)
}
Takeaways
Core Principles:
- Semantic HTML is your foundation - Custom components should enhance, not replace, native functionality
- Focus management is critical - Users must be able to navigate logically with keyboard alone
- Context is everything - Provide clear labels, descriptions, and status announcements
- Test early and often - Accessibility issues are much harder to fix after the fact
Common Mistakes to Avoid:
- Using div elements for interactive components
- Forgetting to handle keyboard events
- Missing focus indicators
- Relying solely on color for important information
- Not testing with actual assistive technologies
Subscribe to Pragmatic Web for more deep dives into building inclusive, robust web applications that work for everyone.