Web Accessibility Best Practices
Build websites that everyone can use! Learn accessibility best practices including WCAG guidelines, ARIA attributes, semantic HTML, keyboard navigation, and testing techniques to create inclusive web experiences.
Why Accessibility Matters
Over 1 billion people worldwide have disabilities. Accessible websites benefit everyone:
- Legal requirement — Many countries mandate accessibility
- Larger audience — Don’t exclude potential users
- Better UX — Accessibility improvements help all users
- SEO benefits — Semantic HTML helps search engines
- Ethical responsibility — The web should be for everyone
Accessibility (often abbreviated as “a11y”) isn’t optional—it’s essential.
WCAG Principles: POUR
Web Content Accessibility Guidelines (WCAG) are built on four principles:
- Perceivable — Information must be presentable to users
- Operable — Interface must be usable
- Understandable — Content must be comprehensible
- Robust — Content must work with assistive technologies
Let’s apply each principle!
Semantic HTML Foundation
Use semantic elements for better accessibility:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Page Structure</title>
</head>
<body>
<!-- Recommended approach: Semantic HTML structure -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Article Title</h1>
<section>
<h2>Introduction</h2>
<p>Article introduction...</p>
</section>
<section>
<h2>Main Content</h2>
<p>Main article content...</p>
</section>
</article>
<aside>
<h2>Related Articles</h2>
<ul>
<li><a href="/related1">Related Article 1</a></li>
<li><a href="/related2">Related Article 2</a></li>
</ul>
</aside>
</main>
<footer>
<p>© 2025 Company Name</p>
</footer>
<!-- Problematic example: Non-semantic structure -->
<!--
<div class="header">
<div class="nav">...</div>
</div>
<div class="content">...</div>
<div class="footer">...</div>
-->
</body>
</html>Why semantic HTML matters:
- Screen readers understand structure
- Keyboard navigation works better
- Clearer code for developers
- Better SEO
Alt Text for Images
Every image needs descriptive alternative text:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Alt Text Examples</title>
</head>
<body>
<!-- Recommended approach: Descriptive alt text -->
<img
src="golden-retriever-playing-fetch.jpg"
alt="Golden retriever running through grass with tennis ball in mouth"
width="600"
height="400">
<!-- Recommended approach: Informative alt for charts -->
<img
src="sales-chart-2024.png"
alt="Bar chart showing 25% increase in Q4 2024 sales compared to Q3"
width="800"
height="600">
<!-- Recommended approach: Empty alt for decorative images -->
<img
src="decorative-border.svg"
alt=""
role="presentation">
<!-- Recommended approach: Alt for functional images -->
<a href="/search">
<img
src="search-icon.svg"
alt="Search"
width="24"
height="24">
</a>
<!-- Problematic example: Missing alt -->
<!--
<img src="important-diagram.jpg">
-->
<!-- Problematic example: Meaningless alt -->
<!--
<img src="photo.jpg" alt="image">
<img src="pic1.jpg" alt="pic1">
-->
<!-- Problematic example: Redundant text -->
<!--
<img src="dog.jpg" alt="Image of a dog">
<img src="chart.jpg" alt="Photo of a chart">
-->
<!-- Complex image with detailed description -->
<figure>
<img
src="complex-infographic.jpg"
alt="Infographic comparing renewable energy adoption across countries"
aria-describedby="infographic-desc">
<figcaption id="infographic-desc">
Detailed data visualization showing solar, wind, and hydro power
adoption rates in USA (35%), Germany (42%), and China (28%)
over the last decade.
</figcaption>
</figure>
</body>
</html>Alt text guidelines:
- Be specific and descriptive
- Describe content and function
- Keep under 150 characters when possible
- Empty alt (
alt="") for decorative images - Never “image of” or “picture of”
ARIA Labels and Roles
ARIA (Accessible Rich Internet Applications) enhances HTML accessibility:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ARIA Attributes</title>
</head>
<body>
<!-- aria-label for context -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
</ul>
</nav>
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy</a></li>
<li><a href="/terms">Terms</a></li>
</ul>
</nav>
<!-- aria-labelledby (reference to visible text) -->
<section aria-labelledby="features-heading">
<h2 id="features-heading">Key Features</h2>
<p>Our product offers...</p>
</section>
<!-- aria-describedby (additional description) -->
<button
aria-label="Delete item"
aria-describedby="delete-warning">
🗑️
</button>
<span id="delete-warning" class="sr-only">
This action cannot be undone
</span>
<!-- aria-expanded for collapsible content -->
<button
aria-expanded="false"
aria-controls="dropdown-menu"
onclick="toggleDropdown(this)">
Menu
</button>
<ul id="dropdown-menu" hidden>
<li><a href="/option1">Option 1</a></li>
<li><a href="/option2">Option 2</a></li>
</ul>
<!-- aria-live for dynamic content -->
<div
role="status"
aria-live="polite"
aria-atomic="true"
id="status-message">
<!-- Dynamic messages appear here -->
</div>
<!-- aria-current for navigation -->
<nav>
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<!-- Custom widget with role -->
<div role="tablist">
<button
role="tab"
aria-selected="true"
aria-controls="panel1"
id="tab1">
Tab 1
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel2"
id="tab2">
Tab 2
</button>
</div>
<div
role="tabpanel"
id="panel1"
aria-labelledby="tab1">
Content for Tab 1
</div>
<script>
function toggleDropdown(button) {
const menu = document.getElementById('dropdown-menu');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
}
</script>
<style>
/* 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-width: 0;
}
</style>
</body>
</html>Key ARIA attributes:
- aria-label — Provides label when none visible
- aria-labelledby — References visible label
- aria-describedby — Additional description
- aria-expanded — Collapsible state
- aria-live — Announces dynamic updates
- aria-current — Indicates current item
- role — Defines widget type
Keyboard Navigation
Ensure all functionality works with keyboard:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keyboard Accessibility</title>
<style>
/* CRITICAL: Visible focus indicators */
*:focus {
outline: 3px solid #4CAF50;
outline-offset: 2px;
}
/* Never remove outlines without replacement! */
/* button:focus { outline: none; } ← BAD! */
/* Custom focus style (if you must) */
.custom-focus:focus {
outline: none;
box-shadow: 0 0 0 3px #4CAF50;
}
/* Skip link (hidden until focused) */
.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;
}
/* Focus within for container styling */
.nav-container:focus-within {
background: #f0f0f0;
}
</style>
</head>
<body>
<!-- Skip to main content link -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>
<nav>
<!-- Keyboard accessible menu -->
<ul>
<li><a href="/" accesskey="h">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact" accesskey="c">Contact</a></li>
</ul>
</nav>
</header>
<main id="main-content" tabindex="-1">
<h1>Main Content</h1>
<!-- Keyboard accessible custom button -->
<div
role="button"
tabindex="0"
onkeydown="handleKeyPress(event)"
onclick="handleClick()">
Custom Button (use real buttons instead!)
</div>
<!-- Proper button (automatically keyboard accessible) -->
<button onclick="handleClick()">
Real Button (Preferred!)
</button>
<!-- Keyboard accessible modal -->
<button onclick="openModal()">Open Modal</button>
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
id="modal"
hidden>
<h2 id="modal-title">Modal Title</h2>
<p>Modal content</p>
<button onclick="closeModal()">Close</button>
</div>
</main>
<script>
// Handle Enter and Space for custom button
function handleKeyPress(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}
function handleClick() {
alert('Button clicked!');
}
// Trap focus in modal
function openModal() {
const modal = document.getElementById('modal');
modal.hidden = false;
// Focus first focusable element
const firstFocusable = modal.querySelector('button');
firstFocusable.focus();
// Store last focused element
window.lastFocusedElement = document.activeElement;
}
function closeModal() {
const modal = document.getElementById('modal');
modal.hidden = true;
// Return focus to trigger element
if (window.lastFocusedElement) {
window.lastFocusedElement.focus();
}
}
// Close modal on Escape
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const modal = document.getElementById('modal');
if (!modal.hidden) {
closeModal();
}
}
});
</script>
</body>
</html>Keyboard requirements:
- All interactive elements must be keyboard accessible
- Visible focus indicators (never remove outline!)
- Logical tab order
- Escape key closes modals/menus
- Enter/Space activates buttons
- Arrow keys for custom widgets
Color Contrast and Visual Design
Ensure sufficient contrast for readability:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Color Contrast</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 40px;
line-height: 1.6;
}
/* Recommended example: Sufficient contrast (4.5:1 for normal text) */
.good-contrast {
background: #ffffff;
color: #333333;
padding: 20px;
margin-bottom: 20px;
}
/* Recommended example: High contrast (7:1 for large text) */
.high-contrast {
background: #000000;
color: #ffffff;
padding: 20px;
margin-bottom: 20px;
font-size: 1.5rem;
}
/* Problematic example: Poor contrast */
.bad-contrast {
background: #ffffff;
color: #cccccc; /* Too light! */
padding: 20px;
margin-bottom: 20px;
}
/* Recommended example: Don't rely on color alone */
.status-success {
border-left: 4px solid #4CAF50;
padding-left: 15px;
background: #e8f5e9;
}
.status-success::before {
content: '✓ ';
font-weight: bold;
}
.status-error {
border-left: 4px solid #f44336;
padding-left: 15px;
background: #ffebee;
}
.status-error::before {
content: '✗ ';
font-weight: bold;
}
/* Recommended example: Underline links, don't rely on color */
a {
color: #0066cc;
text-decoration: underline;
}
a:hover {
background: #e3f2fd;
}
/* Recommended example: Large touch targets (44x44px minimum) */
button {
min-width: 44px;
min-height: 44px;
padding: 12px 24px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Color Contrast Examples</h1>
<div class="good-contrast">
<strong>Good Contrast:</strong> This text has sufficient contrast
ratio (4.5:1) and is easy to read for most users.
</div>
<div class="high-contrast">
<strong>High Contrast:</strong> Large text with excellent
contrast ratio (21:1).
</div>
<div class="bad-contrast">
<strong>Poor Contrast:</strong> This text is hard to read due to
insufficient contrast. Avoid light gray on white!
</div>
<h2>Don't Rely on Color Alone</h2>
<div class="status-success">
Success! Your form has been submitted. Icon and border provide
additional cues beyond just green color.
</div>
<div class="status-error">
Error! Please fix the form errors before submitting. Icon and
border help color-blind users identify the error state.
</div>
<h2>Link Styling</h2>
<p>
Links should be <a href="#">underlined and colored</a> so
color-blind users can identify them. Hover states provide
additional feedback.
</p>
</body>
</html>Contrast requirements:
- Normal text: 4.5:1 contrast ratio minimum
- Large text (18pt+): 3:1 contrast ratio minimum
- UI components: 3:1 contrast ratio
- Never rely on color alone — Use icons, patterns, text
- Test with tools like WebAIM Contrast Checker
Form Accessibility
Make forms usable for everyone:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Forms</title>
<style>
form {
max-width: 600px;
margin: 50px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.required {
color: #f44336;
}
input, textarea, select {
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #4CAF50;
}
input:invalid:not(:placeholder-shown) {
border-color: #f44336;
}
.hint {
font-size: 0.9rem;
color: #666;
margin-top: 5px;
}
.error {
color: #f44336;
font-size: 0.9rem;
margin-top: 5px;
display: none;
}
input:invalid:not(:placeholder-shown) + .error {
display: block;
}
fieldset {
border: 2px solid #ddd;
border-radius: 4px;
padding: 20px;
margin-bottom: 25px;
}
legend {
font-weight: bold;
padding: 0 10px;
}
.radio-group label,
.checkbox-group label {
display: flex;
align-items: center;
gap: 10px;
font-weight: normal;
margin-bottom: 10px;
}
button {
padding: 12px 30px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
}
</style>
</head>
<body>
<form>
<h1>Accessible Contact Form</h1>
<!-- Text input with proper label -->
<div class="form-group">
<label for="name">
Name <span class="required" aria-label="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-describedby="name-hint"
placeholder=" ">
<div id="name-hint" class="hint">Please enter your full name</div>
<div class="error" role="alert">Name is required</div>
</div>
<!-- Email with validation -->
<div class="form-group">
<label for="email">
Email <span class="required" aria-label="required">*</span>
</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-hint"
placeholder=" ">
<div id="email-hint" class="hint">We'll never share your email</div>
<div class="error" role="alert">Valid email required</div>
</div>
<!-- Fieldset for radio buttons -->
<fieldset>
<legend>Preferred Contact Method</legend>
<div class="radio-group">
<label>
<input
type="radio"
name="contact-method"
value="email"
checked>
Email
</label>
<label>
<input
type="radio"
name="contact-method"
value="phone">
Phone
</label>
<label>
<input
type="radio"
name="contact-method"
value="mail">
Mail
</label>
</div>
</fieldset>
<!-- Checkboxes with fieldset -->
<fieldset>
<legend>Interests</legend>
<div class="checkbox-group">
<label>
<input type="checkbox" name="interests" value="web">
Web Development
</label>
<label>
<input type="checkbox" name="interests" value="design">
Design
</label>
<label>
<input type="checkbox" name="interests" value="marketing">
Marketing
</label>
</div>
</fieldset>
<!-- Textarea -->
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
aria-describedby="message-hint"></textarea>
<div id="message-hint" class="hint">
Tell us how we can help you
</div>
</div>
<button type="submit">Send Message</button>
</form>
</body>
</html>Form accessibility checklist:
- Every input has a proper
<label> - Use
aria-requiredfor required fields - Provide hints with
aria-describedby - Show errors with
role="alert" - Group related inputs with
<fieldset> - Validate accessibly
- Large touch targets (44x44px)
Testing for Accessibility
Test your sites with these tools and techniques:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessibility Testing Guide</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
.testing-method {
background: #f8f9fa;
padding: 25px;
margin-bottom: 25px;
border-radius: 8px;
border-left: 4px solid #4CAF50;
}
.testing-method h2 {
margin-top: 0;
color: #333;
}
code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
ul {
margin: 15px 0;
}
li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<h1>How to Test Accessibility</h1>
<div class="testing-method">
<h2>1. Keyboard-Only Navigation</h2>
<p><strong>Test:</strong> Unplug your mouse and navigate using only keyboard</p>
<ul>
<li><code>Tab</code> - Move forward through interactive elements</li>
<li><code>Shift + Tab</code> - Move backward</li>
<li><code>Enter</code> - Activate links and buttons</li>
<li><code>Space</code> - Activate buttons, check checkboxes</li>
<li><code>Arrow keys</code> - Navigate within custom widgets</li>
<li><code>Escape</code> - Close modals and dropdowns</li>
</ul>
<p><strong>Check:</strong> Can you access all features? Is focus visible?</p>
</div>
<div class="testing-method">
<h2>2. Screen Reader Testing</h2>
<p><strong>Tools:</strong></p>
<ul>
<li><strong>NVDA</strong> (Windows, Free) - nvaccess.org</li>
<li><strong>JAWS</strong> (Windows, Paid) - freedomscientific.com</li>
<li><strong>VoiceOver</strong> (macOS/iOS, Built-in) - Cmd+F5</li>
<li><strong>TalkBack</strong> (Android, Built-in)</li>
</ul>
<p><strong>Listen for:</strong></p>
<ul>
<li>Are headings announced correctly?</li>
<li>Do images have alt text?</li>
<li>Are form labels read?</li>
<li>Is the reading order logical?</li>
</ul>
</div>
<div class="testing-method">
<h2>3. Automated Testing Tools</h2>
<p><strong>Browser Extensions:</strong></p>
<ul>
<li><strong>axe DevTools</strong> - Free, comprehensive</li>
<li><strong>WAVE</strong> - Visual feedback tool</li>
<li><strong>Lighthouse</strong> - Built into Chrome DevTools</li>
</ul>
<p><strong>Command-line:</strong></p>
<ul>
<li><strong>axe-core</strong> - Automated testing library</li>
<li><strong>pa11y</strong> - CI/CD accessibility testing</li>
</ul>
</div>
<div class="testing-method">
<h2>4. Color Contrast Checkers</h2>
<ul>
<li><strong>WebAIM Contrast Checker</strong> - webaim.org/resources/contrastchecker</li>
<li><strong>Colour Contrast Analyzer</strong> - Desktop app</li>
<li><strong>Chrome DevTools</strong> - Inspect element contrast</li>
</ul>
</div>
<div class="testing-method">
<h2>5. Vision Simulators</h2>
<p>Test how your site appears with vision impairments:</p>
<ul>
<li><strong>NoCoffee</strong> - Chrome extension</li>
<li><strong>Sim Daltonism</strong> - macOS app for color blindness</li>
<li><strong>Chrome DevTools</strong> - Emulate vision deficiencies</li>
</ul>
</div>
<div class="testing-method">
<h2>6. Mobile Accessibility</h2>
<ul>
<li>Test with VoiceOver (iOS) or TalkBack (Android)</li>
<li>Check touch target sizes (44x44px minimum)</li>
<li>Test landscape and portrait orientations</li>
<li>Verify zoom functionality (up to 200%)</li>
</ul>
</div>
<div class="testing-method">
<h2>7. HTML Validation</h2>
<ul>
<li><strong>W3C Validator</strong> - validator.w3.org</li>
<li><strong>HTML5 Outliner</strong> - Check heading structure</li>
</ul>
<p>Valid HTML is the foundation of accessibility!</p>
</div>
</body>
</html>Common Accessibility Mistakes
Mistake 1: Removing Focus Outlines
Problematic example:
*:focus { outline: none; }Improved example:
*:focus {
outline: 3px solid #4CAF50;
outline-offset: 2px;
}Why: Keyboard users need to see where they are!
Mistake 2: Using Divs for Buttons
Problematic example:
<div onclick="submit()">Submit</div>Improved example:
<button type="submit">Submit</button>Why: Buttons are keyboard accessible by default.
Mistake 3: Empty Links
Problematic example:
<a href="/profile"><img src="avatar.jpg"></a>Improved example:
<a href="/profile">
<img src="avatar.jpg" alt="View profile">
</a>Why: Screen readers need text content or alt text.
Mistake 4: Poor Heading Structure
Problematic example:
<h1>Title</h1>
<h4>Subtitle</h4>Improved example:
<h1>Title</h1>
<h2>Subtitle</h2>Why: Don’t skip heading levels.
Try It Yourself
Ready to build accessible sites? Try these challenges:
Challenge 1: Accessible Navigation (Beginner)
Create a navigation menu with:
- Semantic
<nav>element - Proper heading structure
- Keyboard accessible menu
- Current page indicator (aria-current)
- Skip to content link
Challenge 2: Accessible Form (Intermediate)
Build a complete form featuring:
- All inputs properly labeled
- Error messages with aria-live
- Fieldsets for grouped inputs
- Custom validation messages
- Keyboard accessible throughout
Challenge 3: Accessible Widget Library (Advanced)
Create accessible components:
- Modal dialog with focus trap
- Accordion with keyboard support
- Custom dropdown with ARIA
- Tabs with proper ARIA roles
- Toast notifications
Bonus: Test everything with a screen reader!
What You Learned
Congratulations! You now know how to:
- Apply WCAG accessibility principles
- Use semantic HTML for better a11y
- Write effective alt text
- Implement ARIA attributes correctly
- Ensure keyboard navigation works
- Meet color contrast requirements
- Build accessible forms
- Test with screen readers and tools
- Avoid common accessibility mistakes
- Create inclusive web experiences
Next Steps
Now that you understand accessibility, explore these related tutorials:
- Accessible Forms — Deep dive into form accessibility
- Semantic Elements — Master semantic HTML
- Building a Complete Website — Apply accessibility principles
Ready to build accessible websites? Start creating in our interactive HTML editor!