Accessibility is not a feature, it is a requirement
Stop treating accessibility as nice-to-have. Learn the essential WCAG checklist, keyboard navigation, and screen reader testing that every web app needs.
Accessibility is not a feature, it is a requirement
Last Updated: November 2025
Scope: Web Standards, React, HTML/CSS
The Reality Check
15% of the world’s population has some form of disability. If your app isn’t accessible, you’re not shipping a complete product.
The Audit
Step 1: Keyboard Navigation Test
Close your laptop, disconnect your mouse. Can you:
- Navigate every interactive element using
Tab? - Activate buttons with
EnterorSpace? - Close modals with
Escape?
🚩 Red Flags:
- Focus indicator invisible or missing
- Can’t reach certain UI elements without a mouse
- Tab order jumps around illogically
Step 2: Screen Reader Test
macOS: Enable VoiceOver (Cmd + F5)
Windows: Enable Narrator (Win + Ctrl + Enter)
Navigate your homepage. Does the screen reader:
- Announce button purposes clearly?
- Read form labels correctly?
- Skip decorative images?
🚩 Red Flags:
- “Button” announced without context
- “Image” with no alternative text
- Form inputs with no labels
The Non-Negotiables
1. Semantic HTML (The Foundation)
// ❌ Bad: Divs everywhere
<div onClick={handleClick}>Submit</div>
<div className="heading">Welcome</div>
// ✅ Good: Proper elements
<button onClick={handleClick}>Submit</button>
<h1>Welcome</h1>
Why it matters: Screen readers use HTML semantics to understand page structure.
2. Color Contrast (The Vision Test)
Minimum Requirements (WCAG AA):
- Normal text: 4.5:1 contrast ratio
- Large text (18px+): 3:1 contrast ratio
/* ❌ Bad: Fails contrast check */
.text {
color: #999;
background: #fff;
} /* 2.8:1 ratio */
/* ✅ Good: Passes WCAG AA */
.text {
color: #595959;
background: #fff;
} /* 7:1 ratio */
Tool: Use Chrome DevTools Color Picker (shows contrast ratio automatically).
3. Keyboard Focus Indicators
/* ❌ Bad: Removes focus outline */
button:focus {
outline: none;
}
/* ✅ Good: Custom accessible focus */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
4. Form Labels (Always Required)
// ❌ Bad: Placeholder as label
<input type="email" placeholder="Enter email" />
// ✅ Good: Explicit label
<label htmlFor="email">Email Address</label>
<input type="email" id="email" name="email" />
// ✅ Also Good: Visually hidden label
<label htmlFor="search" className="sr-only">Search</label>
<input type="search" id="search" placeholder="Search..." />
/* Screen reader only class */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
5. Alternative Text for Images
// ❌ Bad: Missing alt
<img src="chart.png" />
// ❌ Bad: Redundant alt
<img src="chart.png" alt="Image of a chart showing sales data" />
// ✅ Good: Descriptive alt
<img src="chart.png" alt="Sales increased 40% in Q3 2025" />
// ✅ Good: Decorative images
<img src="decoration.png" alt="" role="presentation" />
6. ARIA When Needed (Not Always)
Rule: Use HTML first, ARIA second.
// ❌ Bad: Unnecessary ARIA
<button role="button" aria-label="Close">Close</button>
// ✅ Good: Button is already a button
<button>Close</button>
// ✅ Good: ARIA for custom components
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
aria-label="Close notification"
>
×
</div>
Common Patterns (React)
Modal Dialogs
import { useEffect, useRef } from 'react';
const Modal = ({ isOpen, onClose, children }) => {
const closeButtonRef = useRef(null);
useEffect(() => {
if (isOpen) {
// Focus first element when modal opens
closeButtonRef.current?.focus();
// Trap focus inside modal
const handleTab = (e) => {
const focusableElements = modal.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
} else if (!e.shiftKey && document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', handleTab);
return () => document.removeEventListener('keydown', handleTab);
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
className="modal-overlay"
onClick={onClose}
>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2 id="modal-title">Confirm Action</h2>
{children}
<button ref={closeButtonRef} onClick={onClose}>
Close
</button>
</div>
</div>
);
};
Skip Links
// Add at the top of your app
<a href="#main-content" className="skip-link">
Skip to main content
</a>
// Later in the page
<main id="main-content">
{/* Your content */}
</main>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Testing Tools
-
Browser DevTools:
- Chrome Lighthouse (Accessibility audit)
- Firefox Accessibility Inspector
-
Browser Extensions:
- axe DevTools
- WAVE Evaluation Tool
-
Automated Testing:
npm install --save-dev @axe-core/react
// In development only
if (process.env.NODE_ENV !== 'production') {
import('@axe-core/react').then((axe) => {
axe.default(React, ReactDOM, 1000);
});
}
The Checklist (Before Every Deploy)
- All interactive elements accessible via keyboard
- Focus indicators visible on all elements
- All images have appropriate alt text
- Color contrast meets WCAG AA (4.5:1 minimum)
- All form inputs have labels
- Headings follow logical hierarchy (h1 → h2 → h3)
- Modals trap focus and can be closed with Escape
- Tested with screen reader (VoiceOver/NVDA)
- No keyboard traps (can Tab through entire page)
- Error messages are announced to screen readers
The Bottom Line
Accessibility isn’t about compliance checkboxes. It’s about building products that work for everyone. Start with semantic HTML, test with your keyboard, and actually use a screen reader. Your users will notice.
Let's Build Something Scalable
We apply these same engineering principles to client projects. Ready to upgrade your stack?