A reference guide to web accessibility best practices and implementation.
Refer the repo code for the keyboard accessibility examples.
"If a website or part of a website works only with a mouse, then it's not accessible"
WHY IT MATTERS?
The web should empower people, not frustrate or isolate them. Accessibility ensures that no one is locked out of opportunities simply because of how they interact with technology.
Accessibility is about empathy β imagining yourself in someone elseβs place and choosing to build a world that doesnβt leave them behind.
- Disabilities
- Semantic HTML β
- Color Contrast β
- Text Alternatives (Alt Text)
- Form Accessibility
- Heading Structure
- Link Accessibility
- Focus Management β
- Language Attributes
- Responsive Design & Viewport
- Motion & Animation Preferences
- Error Handling & Validation
- Time-Based Media
- Tables
- Landmarks & Regions
- Performance
- Testing Checklist β
- WCAG 2.1 Principles
- Priority Order for Implementation β
- Visual: Users who have low vision, color blindness, or blindness. Screen readers, proper color contrast, scalable texts etc. help them.
- Auditory: Users who are deaf or hard of hearing. Captions, transcripts, and visual cues help them.
- Physical: Users with limited motor control, tremors, paralysis, or who cannot use a mouse. Keyboard navigation, large clickable areas, etc. help them
- Cognitive: Users with learning difficulties, memory issues, ADHD, dyslexia, etc. Simple language, consistent layout, clear navigation helps them.
- Neurological: Users affected by epilepsy, migraines, or other neurological conditions. Avoid flashy or animated content, allow motion reduction and have predictable interactions.
Use the right HTML elements for their purpose. Screen readers and browsers understand structure and meaning.
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Article Title</h1>
<p>Content...</p>
</article>
</main>
<footer>Footer content</footer><div class="header">
<div class="nav">
<div><a href="/">Home</a></div>
</div>
</div>Key semantic elements: <header>, <nav>, <main>, <article>, <section>, <aside>, <footer>, <button>, <form>, <label>, etc.
Ensure text meets WCAG contrast requirements.
- Level AA: 4.5:1 for normal text, 3:1 for large text
- Level AAA: 7:1 for normal text, 4.5:1 for large text
<p style="color: #ccc; background: #fff;">Hard to read</p><p style="color: #000; background: #fff;">Easy to read</p>Tools: WebAIM Contrast Checker, axe DevTools, browser DevTools
Bad:
<span style="color: red;">Error</span>Good:
<span style="color: red;" aria-label="Error">
β οΈ Error: Invalid input
</span>Provide meaningful alt text for images.
<!-- Descriptive alt text -->
<img src="chart.jpg" alt="Sales increased 25% from Q1 to Q2 2024" />
<!-- Decorative images -->
<img src="decoration.jpg" alt="" />
<!-- Complex images with longer description -->
<img src="diagram.jpg" alt="Process flow diagram" />
<figcaption>
Detailed description: The process starts with user input,
goes through validation, then processing...
</figcaption>- Be specific and concise
- Describe the purpose, not just appearance
- Use empty
alt=""for decorative images - Provide longer descriptions for complex images
Make forms accessible with proper labels, error handling, and instructions.
<form>
<fieldset>
<legend>Contact Information</legend>
<label for="email">
Email Address
<span aria-label="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
aria-required="true"
aria-describedby="email-help email-error"
/>
<div id="email-help">We'll never share your email</div>
<div id="email-error" role="alert" aria-live="polite"></div>
<label for="phone">Phone Number</label>
<input
type="tel"
id="phone"
name="phone"
aria-describedby="phone-format"
/>
<div id="phone-format">Format: (555) 123-4567</div>
</fieldset>
<button type="submit">Submit</button>
</form>- Always use
<label>elements - Group related fields with
<fieldset>and<legend> - Provide clear error messages
- Use
aria-required,aria-invalid,aria-describedby - Associate help text with inputs
Use a logical heading hierarchy.
<h1>Main Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>
<h2>Another Section</h2>
<h3>Subsection</h3><h1>Main Title</h1>
<h3>Skipped h2!</h3> <!-- Don't skip h2 -->- One
<h1>per page - Don't skip heading levels
- Use headings to structure content, not for styling
Make links descriptive and distinguishable.
<a href="/page">Click here</a>
<a href="/page">Read more</a><a href="/page">Learn about our accessibility features</a>
<a href="/page">Read the full article about web accessibility</a>
<!-- Links that open in new window -->
<a
href="/external"
target="_blank"
rel="noopener noreferrer"
aria-label="Opens in new window"
>
External Resource
</a>- Use descriptive link text
- Indicate when links open in new windows
- Make links visually distinct (not just color)
- Provide skip links for navigation
Manage focus for dynamic content and modals.
<a href="#main-content" class="skip-link">Skip to main content</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
</style>
<main id="main-content">
<!-- Content -->
</main>// Trap focus within modal
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
});
}Specify the page language and language changes.
<html lang="en">
<head>
<title>My Page</title>
</head>
<body>
<p>This is in English.</p>
<p lang="es">Esto estΓ‘ en espaΓ±ol.</p>
<p lang="fr">Ceci est en franΓ§ais.</p>
</body>
</html>Ensure content works on all screen sizes.
<meta name="viewport" content="width=device-width, initial-scale=1" />- Text should be readable without zooming
- Touch targets should be at least 44x44px
- Content should reflow properly
- Avoid horizontal scrolling
Respect user preferences for reduced motion.
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Or disable specific animations */
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: none;
}
}Provide clear, accessible error messages.
<form>
<label for="email">Email</label>
<input
type="email"
id="email"
aria-invalid="false"
aria-describedby="email-error"
aria-required="true"
/>
<div
id="email-error"
role="alert"
aria-live="polite"
class="error-message"
>
<!-- Error message appears here -->
</div>
</form>
<script>
function validateEmail(input) {
const errorDiv = document.getElementById('email-error');
const isValid = input.validity.valid;
input.setAttribute('aria-invalid', !isValid);
if (!isValid) {
errorDiv.textContent = 'Please enter a valid email address';
errorDiv.style.display = 'block';
} else {
errorDiv.textContent = '';
errorDiv.style.display = 'none';
}
}
</script>Provide captions, transcripts, and audio descriptions.
<video controls>
<source src="video.mp4" type="video/mp4" />
<track
kind="captions"
src="captions.vtt"
srclang="en"
label="English"
default
/>
<track
kind="descriptions"
src="descriptions.vtt"
srclang="en"
/>
</video>
<audio controls>
<source src="audio.mp3" type="audio/mpeg" />
<a href="transcript.txt">Transcript available</a>
</audio>Make tables accessible with proper structure.
<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>
</tbody>
</table>Use ARIA landmarks to identify page regions.
<header role="banner">
<!-- Site header -->
</header>
<nav role="navigation" aria-label="Main navigation">
<!-- Navigation -->
</nav>
<main role="main">
<!-- Main content -->
</main>
<aside role="complementary" aria-label="Related articles">
<!-- Sidebar -->
</aside>
<footer role="contentinfo">
<!-- Footer -->
</footer>Fast loading benefits all users, especially those on slower connections.
- Optimize images
- Minimize JavaScript
- Use lazy loading
- Provide loading states
- Ensure critical content loads first
- axe DevTools - Browser extension for accessibility testing
- WAVE - Web Accessibility Evaluation Tool
- Lighthouse - Built into Chrome DevTools
- Pa11y - Command-line accessibility testing
- β Keyboard-only navigation
- β Screen reader testing (NVDA, JAWS, VoiceOver)
- β Color contrast checking
- β Zoom testing (up to 200%)
- β Mobile device testing
The Web Content Accessibility Guidelines (WCAG) are organized around four principles:
- Text alternatives, captions, color contrast, text resizing
- Keyboard accessible, no seizures, navigable, enough time
- Readable, predictable, input assistance
- Valid HTML, proper use of ARIA, compatible with tools
- Semantic HTML
- Keyboard navigation
- Focus management
- Color contrast
- Alt text
- Form labels
- Heading structure
- ARIA labels
- Error handling
- Skip links
- Language attributes
- Motion preferences
- Captions
- Advanced ARIA patterns
- WCAG 2.1 Guidelines
- WebAIM
- A11y Project
- MDN Accessibility
- ARIA Authoring Practices Guide
- Front-end specialisations: Accessibility
aria-label- Provides an accessible namearia-labelledby- References elements that label this elementaria-describedby- References elements that describe this elementaria-hidden- Hides element from assistive technologiesaria-expanded- Indicates expandable/collapsible statearia-controls- Identifies controlled elementsaria-live- Indicates live region (polite, assertive, off)aria-required- Indicates required form fieldaria-invalid- Indicates invalid inputaria-current- Indicates current item in set
Remember: Start with the basics (semantic HTML, keyboard navigation, proper labels) and build from there. Each improvement makes your site more accessible to everyone!
