Learn WCAG 2.1 fundamentals, semantic HTML, ARIA roles, keyboard navigation, color contrast, and how to test accessibility in your web projects.
Over 1 billion people worldwide live with some form of disability. Web accessibility isn't just about compliance — it's about not locking people out of your product. It also improves SEO, usability for all users, and in many jurisdictions, it's legally required.
Perceivable — Users can perceive all information Operable — Users can operate the interface Understandable — Users understand content and UI Robust — Works with current and future assistive technologies
WCAG 2.1 Level AA is the standard target for most web applications.
Use the right HTML element for the job. Screen readers announce the element's role:
<!-- Bad -->
<div onclick="submit()">Submit</div>
<div class="header">My Site</div>
<div class="nav">...</div>
<!-- Good -->
<button type="submit">Submit</button>
<header><h1>My Site</h1></header>
<nav aria-label="Main navigation">...</nav>
Semantic elements provide keyboard interaction and ARIA roles for free.
<!-- Informative image -->
<img src="chart.png" alt="Bar chart showing 40% increase in revenue Q1 2025" />
<!-- Decorative image — empty alt tells screen readers to skip it -->
<img src="divider.svg" alt="" role="presentation" />
<!-- Icon button — describe the action -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
All interactive elements must be reachable and operable with a keyboard alone:
<!-- Natural tab order — don't use tabindex > 0 -->
<button>First</button>
<button>Second</button>
<button>Third</button>
<!-- Custom focusable element -->
<div role="button" tabindex="0" onkeydown="handleKey(event)">
Click me
</div>
Test by pressing Tab through your entire UI. Every interactive element should be reachable, and focus should be clearly visible.
When dialogs open, move focus inside them. When they close, return focus to the trigger:
function Dialog({ isOpen, onClose, triggerRef }) {
const dialogRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
dialogRef.current?.focus();
} else {
triggerRef.current?.focus();
}
}, [isOpen]);
return isOpen ? (
<div role="dialog" aria-modal="true" ref={dialogRef} tabIndex={-1}>
{/* content */}
</div>
) : null;
}
WCAG AA requires:
Tools:
Never convey information by color alone — add icons, patterns, or text labels.
<!-- Label a landmark region -->
<nav aria-label="Breadcrumb">...</nav>
<!-- Describe an input more fully -->
<input
type="password"
aria-describedby="pwd-hint"
/>
<p id="pwd-hint">Must be 8+ characters with one number</p>
<!-- Live regions for dynamic updates -->
<div aria-live="polite" aria-atomic="true">
Form submitted successfully!
</div>
<!-- Associate every input with a label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" required autocomplete="email" />
<!-- Group related inputs -->
<fieldset>
<legend>Shipping address</legend>
<!-- inputs -->
</fieldset>
<!-- Error messages -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
Please enter a valid email address.
</span>
Start with semantic HTML and you'll get 60% of accessibility for free. Add proper labels, keyboard navigation, and visible focus states for another 30%. The remaining 10% is nuanced ARIA patterns for complex widgets. Automated tools catch about 30-40% of issues — manual testing with a keyboard and screen reader is irreplaceable.