Compliance Is the Floor, Not the Ceiling
Most teams treat accessibility as a compliance task — run an automated scan, fix the flagged issues, check the box. But automated tools catch at most 30-40% of real accessibility barriers. The rest requires understanding how people actually use the web: with screen readers, keyboard navigation, magnification, voice control, and switch devices.
After building interfaces used by millions of people across diverse contexts — low vision, motor impairments, cognitive differences, and temporary disabilities like a broken arm — I've learned that accessibility isn't about following rules. It's about empathy translated into code.
#1 Semantic HTML: The Foundation Everyone Skips
The single most impactful accessibility improvement is using the correct HTML elements. A <div> with an onClick handler is not a button. A styled <span> is not a heading. Screen readers, browsers, and assistive technologies all rely on semantic meaning.
Use Native Elements First
Native HTML elements come with built-in accessibility: keyboard handling, focus management, and ARIA roles — for free.
// ❌ Inaccessible: no keyboard support, no role, no focus
<div className="btn" onClick={handleClick}>
Submit
</div>
// ✅ Accessible: focusable, keyboard-operable, announced as button
<button
type="submit"
className="btn"
onClick={handleClick}
disabled={isLoading}
>
{isLoading ? 'Submitting...' : 'Submit'}
</button>The same principle applies to every element:
- Use <a> for navigation, <button> for actions — never the other way around
- Use <nav>, <main>, <aside>, <header>, <footer> for page landmarks
- Use <h1>–<h6> in logical order — screen reader users navigate by headings
- Use <ul>/<ol> for lists — screen readers announce 'list, 5 items'
- Use <table> with <th> and scope for actual tabular data
- Screen readers correctly announce element purposes
- Keyboard navigation works without custom JavaScript
- Browser features (form validation, autofill) work naturally
#2 Keyboard Navigation and Focus Management
Approximately 15-20% of users rely on keyboard navigation — not just screen reader users, but power users, people with motor impairments, and anyone who can't use a mouse. If your interface isn't keyboard-operable, it's broken for these users.
Focus Trapping in Modals
When a modal opens, focus must move into it and stay trapped until the modal closes. When it closes, focus must return to the trigger element.
import { useEffect, useRef } from 'react';
export function useFocusTrap(isOpen: boolean) {
const containerRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!isOpen || !containerRef.current) return;
// Store the element that triggered the modal
triggerRef.current = document.activeElement as HTMLElement;
const focusable = containerRef.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
first?.focus();
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
// Return focus to trigger when modal closes
triggerRef.current?.focus();
};
}, [isOpen]);
return containerRef;
}Visible Focus Indicators
Never remove focus outlines without providing a visible alternative. The :focus-visible selector lets you show focus rings only for keyboard users, not mouse clicks.
/* Remove default outline for mouse users */
:focus {
outline: none;
}
/* Show clear focus ring for keyboard users */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-radius: 4px;
}
/* High contrast mode support */
@media (forced-colors: active) {
:focus-visible {
outline: 3px solid LinkText;
}
}- Modal interactions are fully keyboard-operable
- Focus is never lost or stuck in invisible elements
- Focus indicators are visible for keyboard users, hidden for mouse users
#3 Dynamic Content and Live Regions
Single-page applications constantly update content without page reloads. Screen readers have no way to know that something changed unless you explicitly announce it using ARIA live regions.
ARIA Live Regions for Announcements
Use aria-live to announce dynamic changes. Use polite for non-urgent updates and assertive for critical alerts.
export function ToastContainer({ toasts }: { toasts: Toast[] }) {
return (
<div
role="status"
aria-live="polite"
aria-atomic="false"
className="fixed bottom-4 right-4 z-50 space-y-2"
>
{toasts.map((toast) => (
<div
key={toast.id}
role={toast.type === 'error' ? 'alert' : undefined}
className="rounded-lg border bg-background p-4 shadow-lg"
>
<p className="text-sm font-medium">{toast.message}</p>
</div>
))}
</div>
);
}- Form submission results: 'Form submitted successfully' or 'Please fix 2 errors'
- Loading states: 'Loading results...' then 'Showing 10 results'
- Cart updates: 'Item added to cart. Cart total: 3 items'
- Real-time data: Stock prices, notifications, chat messages
- Screen reader users are informed of all meaningful content changes
- Error states are announced immediately without losing context
- Loading and success states provide clear, non-visual feedback
Testing Accessibility: A Practical Workflow
Accessibility testing should be layered. No single tool catches everything:
- Automated: axe-core in CI/CD catches ~35% of issues (missing alt text, contrast, ARIA misuse)
- Keyboard: Tab through every flow — can you complete the task without a mouse?
- Screen reader: Test with VoiceOver (macOS) or NVDA (Windows) for flow comprehension
- Zoom: Test at 200% and 400% zoom — does content reflow without horizontal scrolling?
- Reduced motion: Test with prefers-reduced-motion — are animations respectable?
Key Takeaways
- 1Automated tools catch at most 35% of accessibility issues — manual testing is essential
- 2Semantic HTML provides 80% of accessibility for free — use native elements first
- 3Keyboard navigation and focus management are critical for 15-20% of users
- 4ARIA live regions are essential for dynamic SPAs — screen readers can't detect DOM changes
- 5Accessibility is not a feature — it's a quality attribute that affects every user