Learn essential accessibility techniques to create inclusive web experiences that work for everyone, including users with disabilities.
Web accessibility ensures that websites and applications are usable by everyone, including people with disabilities. This comprehensive guide will help you understand and implement accessibility best practices in your projects.
Web accessibility means that websites, tools, and technologies are designed and developed so that people with disabilities can use them. This includes:
Accessibility isn't just the right thing to do—it's good business:
Semantic HTML provides meaning and structure:
<!-- ✅ Good - semantic structure -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Article Title</h1>
<p>Article content...</p>
</article>
</main>
<footer>
<p>© 2024 My Company</p>
</footer>
<!-- ❌ Bad - div soup -->
<div class="header">
<div class="nav">
<div class="nav-item">Home</div>
<div class="nav-item">About</div>
</div>
</div>
Create a logical heading structure:
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>
<h3>Another Subsection</h3>
<h2>Another Section</h2>
<h3>Subsection</h3>
Provide additional context for screen readers:
<!-- Button with descriptive label -->
<button aria-label="Close dialog">×</button>
<!-- Form field with description -->
<label for="password">Password</label>
<input
type="password"
id="password"
aria-describedby="password-help"
required
>
<div id="password-help">
Password must be at least 8 characters long
</div>
<!-- Image with descriptive text -->
<img
src="chart.png"
alt="Sales increased by 25% in Q3 2024"
>
Convey dynamic state information:
<!-- Expandable section -->
<button
aria-expanded="false"
aria-controls="content"
onclick="toggleContent()"
>
Toggle Content
</button>
<div id="content" aria-hidden="true">
Hidden content...
</div>
<!-- Form validation -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
>
<div id="email-error" role="alert">
Please enter a valid email address
</div>
Ensure all interactive elements are keyboard accessible:
/* Visible focus indicators */
:focus {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
/* Custom focus styles */
.button:focus {
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.3);
}
Maintain logical tab order:
<!-- Logical tab sequence -->
<form>
<label for="name">Name</label>
<input type="text" id="name" tabindex="1">
<label for="email">Email</label>
<input type="email" id="email" tabindex="2">
<button type="submit" tabindex="3">Submit</button>
</form>
Provide shortcuts for keyboard users:
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<nav>...</nav>
<main id="main-content">
<!-- Main content -->
</main>
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
z-index: 1000;
}
.skip-link:focus {
top: 6px;
}
Ensure sufficient contrast between text and background:
/* ✅ Good contrast */
.text-dark {
color: #000000; /* Black text */
background: #ffffff; /* White background */
/* Contrast ratio: 21:1 */
}
.text-medium {
color: #333333; /* Dark gray */
background: #ffffff; /* White background */
/* Contrast ratio: 12.6:1 */
}
/* ❌ Poor contrast */
.text-poor {
color: #999999; /* Light gray */
background: #ffffff; /* White background */
/* Contrast ratio: 2.8:1 - Too low! */
}
Provide additional visual cues:
<!-- ✅ Good - multiple indicators -->
<button class="success">
<span class="icon" aria-hidden="true">✓</span>
Success
</button>
<!-- ❌ Bad - color only -->
<button style="color: green;">Success</button>
Associate labels with form controls:
<!-- ✅ Good - explicit association -->
<label for="username">Username</label>
<input type="text" id="username" name="username">
<!-- ✅ Good - implicit association -->
<label>
Email
<input type="email" name="email">
</label>
<!-- ❌ Bad - no association -->
<div>Username</div>
<input type="text" name="username">
Provide clear error messages:
<form>
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-error"
aria-invalid="true"
required
>
<div id="email-error" class="error-message" role="alert">
Please enter a valid email address
</div>
</div>
</form>
Clearly indicate required fields:
<label for="name">
Name <span class="required" aria-label="required">*</span>
</label>
<input type="text" id="name" required>
Provide meaningful alternative text:
<!-- ✅ Good - descriptive alt text -->
<img src="chart.png" alt="Sales increased by 25% in Q3 2024">
<!-- ✅ Good - decorative image -->
<img src="decoration.png" alt="" role="presentation">
<!-- ❌ Bad - generic alt text -->
<img src="chart.png" alt="chart">
Use long descriptions for complex content:
<img
src="infographic.png"
alt="Infographic showing company growth"
longdesc="#infographic-description"
>
<div id="infographic-description">
<h3>Company Growth Infographic</h3>
<p>This infographic shows our company's growth over the past five years...</p>
</div>
Provide captions and transcripts:
<video controls>
<source src="video.mp4" type="video/mp4">
<track
kind="captions"
src="captions.vtt"
srclang="en"
label="English"
>
<p>Your browser doesn't support video.
<a href="transcript.txt">Read the transcript</a>
</p>
</video>
Use tools to catch accessibility issues:
# Install axe-core for automated testing
npm install --save-dev @axe-core/react
# Install Lighthouse CLI
npm install -g lighthouse
// React component with axe testing
import { useEffect } from 'react'
import axe from '@axe-core/react'
function App() {
useEffect(() => {
axe(React, ReactDOM, 1000)
}, [])
return (
<div>
{/* Your app content */}
</div>
)
}
Test with real assistive technologies:
Use built-in accessibility features:
Make modals accessible:
function Modal({ isOpen, onClose, children }) {
useEffect(() => {
if (isOpen) {
// Trap focus
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
if (focusableElements?.length) {
focusableElements[0].focus()
}
}
}, [isOpen])
if (!isOpen) return null
return (
<div
className="modal-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div
className="modal-content"
onClick={e => e.stopPropagation()}
ref={modalRef}
>
<h2 id="modal-title">Modal Title</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>
)
}
Make tables accessible:
<table>
<caption>Monthly Sales Report</caption>
<thead>
<tr>
<th scope="col">Month</th>
<th scope="col">Sales</th>
<th scope="col">Growth</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">January</th>
<td>$10,000</td>
<td>+5%</td>
</tr>
<tr>
<th scope="row">February</th>
<td>$12,000</td>
<td>+20%</td>
</tr>
</tbody>
</table>
Web accessibility is not a one-time task but an ongoing commitment to inclusive design. By following these guidelines and continuously testing with real users, you can create web experiences that work for everyone.
Remember:
The web should be accessible to everyone, and as developers, we have the power to make that happen.