Accessibility (A11Y)
Optimizing for Screen Readers
Make your websites accessible to blind and visually impaired users. Learn how screen readers work and how to optimize your HTML for the best screen reader experience.
How Screen Readers Work
Screen readers convert text and page structure into speech or braille. They navigate through HTML semantics, ARIA attributes, and heading hierarchies.
Popular Screen Readers:
- NVDA: Free, Windows (most popular)
- JAWS: Commercial, Windows (enterprise standard)
- VoiceOver: Built-in, macOS/iOS
- TalkBack: Built-in, Android
- Narrator: Built-in, Windows
Page Structure for Screen Readers
Landmarks for Navigation
HTML
<!-- Screen readers can jump between landmarks -->
<header role="banner">
<nav role="navigation" aria-label="Main navigation">
<!-- Navigation -->
</nav>
</header>
<main role="main">
<!-- Main content -->
</main>
<aside role="complementary">
<!-- Sidebar -->
</aside>
<footer role="contentinfo">
<!-- Footer -->
</footer>
<!-- Users can: -->
<!-- - List all landmarks -->
<!-- - Jump directly to main content -->
<!-- - Skip repetitive navigation -->Skip Links
HTML
<body>
<!-- Skip link (usually hidden visually) -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>
<nav><!-- Lots of navigation links --></nav>
</header>
<main id="main-content">
<!-- Main content here -->
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: white;
padding: 8px;
text-decoration: none;
z-index: 100;
}
.skip-link:focus {
top: 0; /* Show on keyboard focus */
}
</style>Heading Hierarchy
Screen reader users often navigate by headings. Proper hierarchy is crucial.
✅ Good Heading Structure
HTML
<h1>Page Title</h1>
<h2>Section 1</h2>
<p>Content...</p>
<h3>Subsection 1.1</h3>
<p>Content...</p>
<h3>Subsection 1.2</h3>
<p>Content...</p>
<h2>Section 2</h2>
<p>Content...</p>
<!-- Users can: -->
<!-- - List all headings -->
<!-- - Jump to any heading -->
<!-- - Understand content structure -->⌠Bad: Skipping Levels
HTML
<h1>Page Title</h1>
<h4>Subsection</h4> <!-- ⌠Skipped h2 and h3 -->
<h2>Section</h2> <!-- ⌠Wrong order -->Links and Buttons
Descriptive Link Text
⌠Bad: Vague Link Text
HTML
<a href="/report.pdf">Click here</a>
<a href="/more">Read more</a>
<a href="/learn">Learn more</a>
<!-- Out of context, these mean nothing! -->
<!-- Screen readers can list all links -->✅ Good: Descriptive Links
HTML
<a href="/report.pdf">Download 2024 Annual Report (PDF, 2MB)</a>
<a href="/features">Read more about our features</a>
<a href="/html-guide">Learn more about HTML basics</a>
<!-- Clear and meaningful out of context -->Button vs Link
<!-- Use links for navigation -->
<a href="/page">Go to page</a>
<!-- Use buttons for actions -->
<button onclick="saveData()">Save</button>
<button type="submit">Submit form</button>
<!-- Screen readers announce role differently:
"Link: Go to page"
"Button: Save" -->Images and Alt Text
Informative Images
<!-- Describe what's in the image -->
<img src="chart.png"
alt="Bar chart showing 45% increase in sales from Q3 to Q4 2024">
<!-- Include text from image -->
<img src="quote.png"
alt="Quote: The best way to predict the future is to invent it">Decorative Images
<!-- Empty alt for decorative images -->
<img src="decorative-line.png" alt="">
<!-- Or use aria-hidden -->
<img src="decoration.png" aria-hidden="true" alt="">
<!-- Screen reader skips these -->Complex Images
<!-- Use longdesc or detailed description -->
<img src="complex-diagram.png"
alt="Website architecture diagram"
aria-describedby="diagram-desc">
<div id="diagram-desc">
<p>The diagram shows three layers:</p>
<ul>
<li>Frontend: React application</li>
<li>Backend: Node.js API server</li>
<li>Database: PostgreSQL database</li>
</ul>
</div>Forms for Screen Readers
Labels and Instructions
HTML
<!-- Always use labels -->
<label for="email">Email address:</label>
<input type="email" id="email" name="email" required>
<!-- Provide instructions -->
<label for="password">
Password:
<span class="help-text">(Must be 8+ characters)</span>
</label>
<input type="password"
id="password"
aria-describedby="password-help">
<div id="password-help">
Must contain at least one number and one uppercase letter
</div>Error Messages
HTML
<label for="email">Email:</label>
<input type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error">
<div id="email-error" role="alert">
Please enter a valid email address
</div>
<!-- role="alert" makes it announced immediately -->Grouping Related Fields
HTML
<fieldset>
<legend>Contact Information</legend>
<label for="name">Name:</label>
<input type="text" id="name">
<label for="email">Email:</label>
<input type="email" id="email">
</fieldset>
<!-- Screen reader announces: -->
<!-- "Contact Information, Name, edit text" -->Tables for Screen Readers
HTML
<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>$45,000</td>
<td>+12%</td>
</tr>
</tbody>
</table>
<!-- Screen reader announces: -->
<!-- "Table: Monthly Sales Report, 3 columns, 2 rows" -->
<!-- "Month column header, January row header, $45,000" -->Dynamic Content
Live Regions
HTML
<!-- Status messages -->
<div aria-live="polite" aria-atomic="true">
<p id="status">Ready</p>
</div>
<script>
// Updates are announced
status.textContent = 'Saving...';
setTimeout(() => {
status.textContent = 'Saved successfully!';
}, 2000);
</script>
<!-- aria-live values: -->
<!-- polite: Wait for user to pause -->
<!-- assertive: Interrupt immediately (use sparingly!) -->
<!-- off: No announcements -->Loading States
HTML
<button aria-busy="false" id="submit-btn">
Submit
</button>
<script>
button.onclick = async () => {
button.setAttribute('aria-busy', 'true');
button.textContent = 'Loading...';
await submitForm();
button.setAttribute('aria-busy', 'false');
button.textContent = 'Submit';
};
</script>Testing with Screen Readers
NVDA Basics (Windows)
- Insert + Down Arrow - Read from cursor
- Insert + T - Read title
- H - Next heading
- Shift + H - Previous heading
- K - Next link
- F - Next form field
- T - Next table
- D - Next landmark
- Insert + F7 - List all elements
VoiceOver Basics (Mac)
- Cmd + F5 - Toggle VoiceOver
- VO + A - Start reading
- VO + Right/Left Arrow - Navigate
- VO + Cmd + H - Next heading
- VO + Cmd + L - Next link
- VO + U - Rotor (list elements)
VO = Control + Option
Common Pitfalls
⌠Pitfall 1: CSS-Only Content
CSS
.icon::before {
content: "Important: ";
}
<!-- Screen readers may not announce ::before content -->⌠Pitfall 2: Placeholder as Label
HTML
<input type="text" placeholder="Enter name">
<!-- ⌠No label! Placeholder disappears on input -->⌠Pitfall 3: Div/Span Buttons
HTML
<div onclick="submit()">Submit</div>
<!-- ⌠Not accessible, no keyboard support -->⌠Pitfall 4: Auto-playing Content
HTML
<video autoplay>
<!-- ⌠Disrupts screen reader users -->Best Practices Checklist
- ☑ Use semantic HTML elements
- ☑ Maintain logical heading hierarchy
- ☑ Provide skip links
- ☑ Use descriptive link text
- ☑ Add alt text to all images
- ☑ Label all form inputs
- ☑ Use fieldset for related fields
- ☑ Announce dynamic changes with aria-live
- ☑ Make all interactive elements keyboard accessible
- ☑ Test with actual screen readers
- ☑ Avoid CSS-only important content
- ☑ Don't auto-play media