HTML & JS Topic
HTML Accessibility (A11y)
What is Web Accessibility?
The Problem:
html
<!-- Inaccessible Example -->
<div onclick="submitForm()">Submit</div>
<img src="chart.jpg">
<div class="menu">
<div class="item">Home</div>
<div class="item">About</div>
</div>
<!-- Problems:
1. Div not keyboard accessible
2. Image missing alt text
3. Menu not navigable with screen reader
-->
The Solution:
html
<!-- Accessible Example -->
<button onclick="submitForm()">Submit Form</button>
<img src="chart.jpg" alt="Sales growth chart showing 25% increase in Q4">
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About Us</a></li>
</ul>
</nav>
ARIA (Accessible Rich Internet Applications)
What is ARIA?
A set of attributes that make web content more accessible when HTML semantics aren't enough.
Basic ARIA Roles:
html
<!-- Landmark Roles -->
<div role="banner">...</div> <!-- = <header> -->
<div role="navigation">...</div> <!-- = <nav> -->
<div role="main">...</div> <!-- = <main> -->
<div role="complementary">...</div> <!-- = <aside> -->
<div role="contentinfo">...</div> <!-- = <footer> -->
<div role="search">...</div> <!-- Search area -->
<!-- Widget Roles -->
<button role="button">Click me</button>
<div role="checkbox" aria-checked="false">Option</div>
<div role="tab">Tab 1</div>
<div role="alert">Error message!</div>
<div role="progressbar" aria-valuenow="50">50%</div>
<!-- Live Region Roles -->
<div role="status" aria-live="polite">Changes saved</div>
<div role="alert" aria-live="assertive">Critical error!</div>
<div role="log" aria-live="polite">New message received</div>
ARIA Attributes:
html
<!-- Labels and Descriptions -->
<button aria-label="Close dialog">X</button>
<div aria-labelledby="section1-title">
<h2 id="section1-title">Introduction</h2>
<p>Content...</p>
</div>
<div aria-describedby="instructions">
<input type="text">
<p id="instructions">Enter your full name</p>
</div>
<!-- States and Properties -->
<button aria-expanded="false" aria-controls="dropdown1">
Menu
</button>
<div id="dropdown1" aria-hidden="true">...</div>
<div role="checkbox" aria-checked="mixed">Select All</div>
<div role="slider" aria-valuemin="0" aria-valuemax="100" aria-valuenow="50">
Volume
</div>
<input type="text" aria-required="true" aria-invalid="false">
<input type="text" aria-disabled="true" value="Cannot edit">
<!-- Relationships -->
<div role="menu">
<div role="menuitem" aria-haspopup="true">File</div>
<div role="menu" aria-labelledby="file-menu">
<div role="menuitem">New</div>
</div>
</div>
When to Use ARIA:
html
<!-- 1. When native HTML isn't enough -->
<div class="custom-checkbox"
role="checkbox"
aria-checked="false"
tabindex="0">
Accept terms
</div>
<!-- 2. For custom widgets -->
<div class="tablist" role="tablist">
<div class="tab"
role="tab"
aria-selected="true"
aria-controls="panel1">
Tab 1
</div>
</div>
<!-- 3. For live updates -->
<div class="notification"
role="status"
aria-live="polite">
3 new messages
</div>
<!-- 4. For complex relationships -->
<label for="slider">Volume</label>
<div role="slider"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
aria-labelledby="slider-label">
<div class="slider-track"></div>
</div>
Keyboard Navigation
Focus Management:
html
<!-- Focusable Elements (by default) -->
<a href="#">Link</a>
<button>Button</button>
<input type="text">
<select><option>Select</option></select>
<textarea></textarea>
<!-- Make non-focusable elements focusable -->
<div tabindex="0">Focusable div</div> <!-- In natural tab order -->
<div tabindex="-1">Programmatically focusable</div> <!-- Not in tab order -->
<!-- Remove from tab order -->
<button tabindex="-1">Not in tab order</button>
Tab Order Best Practices:
html
<!-- Logical tab order -->
<header>
<a href="#main" class="skip-link">Skip to content</a>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main id="main">
<h1>Welcome</h1>
<form>
<label for="name">Name:</label>
<input id="name" type="text">
<button type="submit">Submit</button>
</form>
</main>
<!-- Bad tab order (avoid) -->
<button style="position: absolute; right: 0;">Last in HTML, first visually</button>
<div tabindex="10">Arbitrary tabindex > 0</div>
Keyboard Event Handling:
html
<button onclick="handleClick()" onkeydown="handleKeydown(event)">
Click me
</button>
<script>
function handleKeydown(event) {
// Handle Enter and Space (standard button activation)
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}
// Custom widget example
function handleCustomKeydown(event) {
switch(event.key) {
case 'ArrowUp':
// Navigate up
break;
case 'ArrowDown':
// Navigate down
break;
case 'Enter':
case ' ':
// Activate
break;
case 'Escape':
// Close
break;
case 'Home':
// Go to first item
break;
case 'End':
// Go to last item
break;
}
}
</script>
Complete Keyboard Accessible Component:
html
<div class="accordion">
<h3>
<button class="accordion-button"
aria-expanded="false"
aria-controls="panel1"
onclick="togglePanel('panel1')"
onkeydown="accordionKeydown(event, 'panel1')">
Section 1
</button>
</h3>
<div id="panel1" class="accordion-panel" aria-hidden="true">
<p>Content for section 1...</p>
</div>
</div>
<script>
function togglePanel(panelId) {
const panel = document.getElementById(panelId);
const button = panel.previousElementSibling.querySelector('button');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
panel.setAttribute('aria-hidden', isExpanded);
panel.style.display = isExpanded ? 'block' : 'none';
}
function accordionKeydown(event, panelId) {
switch(event.key) {
case 'Enter':
case ' ':
event.preventDefault();
togglePanel(panelId);
break;
case 'ArrowDown':
// Focus next accordion button
break;
case 'ArrowUp':
// Focus previous accordion button
break;
case 'Home':
// Focus first accordion button
break;
case 'End':
// Focus last accordion button
break;
}
}
</script>
Screen Reader Basics
How Screen Readers Work:
text
Screen Reader Output: "Link, Home" "Button, Submit Form" "Heading level 1, Welcome to our site" "Image, Company logo" "Edit, First Name"
Semantic HTML for Screen Readers:
html
<!-- Good screen reader experience -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/products">Products</a></li>
</ul>
</nav>
<main>
<h1>Welcome to our Store</h1>
<img src="sale-banner.jpg" alt="50% off all items this weekend">
<section aria-labelledby="featured-heading">
<h2 id="featured-heading">Featured Products</h2>
<!-- Products -->
</section>
</main>
<!-- Screen reader output:
"Navigation landmark, Main navigation"
"List with 2 items"
"Link, Home, current page"
"Link, Products"
"Main landmark"
"Heading level 1, Welcome to our Store"
"Image, 50% off all items this weekend"
"Region, Featured Products"
"Heading level 2, Featured Products"
-->
Screen Reader Specific Attributes:
html
<!-- Hide decorative elements -->
<div aria-hidden="true">✨ Decorative sparkles ✨</div>
<!-- Live regions for updates -->
<div aria-live="polite">
<span id="message-count">3</span> new messages
</div>
<div aria-live="assertive" role="alert">
<strong>Error:</strong> Form submission failed!
</div>
<!-- Describing complex content -->
<canvas id="chart"
aria-label="Revenue chart showing quarterly growth"
role="img">
</canvas>
<svg aria-labelledby="svg-title svg-desc">
<title id="svg-title">Monthly Sales</title>
<desc id="svg-desc">Bar chart showing January: $10k, February: $15k</desc>
<!-- SVG content -->
</svg>
Skip Navigation Links:
html
<body>
<!-- First focusable element - for keyboard users -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<header>...</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: 1000;
}
.skip-link:focus {
top: 0;
outline: 3px solid #005a9c;
}
</style>
HTML Best Practices for Accessibility
1. Semantic Structure
html
<!-- Bad --> <div class="header">...</div> <div class="content">...</div> <div class="footer">...</div> <!-- Good --> <header>...</header> <main>...</main> <footer>...</footer>
2. Proper Headings
html
<!-- Bad: Skipping levels -->
<h1>Title</h1>
<h3>Subtitle</h3> <!-- Skipped h2! -->
<!-- Good: Proper hierarchy -->
<h1>Main Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>
<!-- Bad: Multiple h1 -->
<h1>Page Title</h1>
<article>
<h1>Article Title</h1> <!-- Multiple h1 on same page -->
</article>
<!-- Good: One h1 per page -->
<h1>Page Title</h1>
<article>
<h2>Article Title</h2> <!-- Use h2 for article titles -->
</article>
3. Forms and Labels
html
<!-- Bad: Missing or poor labels -->
<input type="text" placeholder="Enter name">
<button>Submit</button>
<!-- Good: Properly labeled -->
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<button type="submit">Submit Form</button>
<!-- Group related inputs -->
<fieldset>
<legend>Shipping Address</legend>
<div>
<label for="street">Street Address:</label>
<input type="text" id="street" name="street">
</div>
<div>
<label for="city">City:</label>
<input type="text" id="city" name="city">
</div>
</fieldset>
<!-- Required fields -->
<label for="email">
Email Address <span aria-hidden="true">*</span>
<span class="sr-only">required</span>
</label>
<input type="email" id="email" required
aria-required="true">
<!-- Error messages -->
<label for="password">Password:</label>
<input type="password" id="password"
aria-describedby="password-error"
aria-invalid="true">
<div id="password-error" role="alert">
Password must be at least 8 characters
</div>
4. Images and Media
html
<!-- Bad: Missing or poor alt text -->
<img src="product.jpg">
<img src="icon-check.png" alt="check">
<img src="chart.png" alt="image">
<!-- Good: Descriptive alt text -->
<img src="product.jpg"
alt="Red wireless headphones with noise cancellation">
<img src="icon-check.png" alt="" role="presentation"> <!-- Decorative -->
<img src="chart.png"
alt="Bar chart showing 2023 sales: Q1 $50k, Q2 $65k, Q3 $80k, Q4 $95k">
<!-- Complex images -->
<figure>
<img src="infographic.jpg"
alt="Infographic about water conservation">
<figcaption>
Figure 1: 10 ways to save water at home
</figcaption>
</figure>
<!-- Linked images -->
<a href="/products">
<img src="sale-banner.jpg"
alt="Shop our summer sale - Up to 50% off">
</a>
5. Links and Buttons
html
<!-- Bad: Non-descriptive text -->
<a href="#">Click here</a>
<button>Submit</button>
<!-- Good: Descriptive text -->
<a href="/products/shoes">Browse all shoes</a>
<button type="submit">Complete Purchase</button>
<!-- Bad: Div as button -->
<div onclick="submitForm()">Submit</div>
<!-- Good: Actual button -->
<button onclick="submitForm()">Submit Form</button>
<!-- External links -->
<a href="https://external.com"
target="_blank"
rel="noopener noreferrer">
External Site <span aria-hidden="true">↗</span>
<span class="sr-only">(opens in new window)</span>
</a>
<!-- Download links -->
<a href="/files/report.pdf" download>
Download Report (PDF, 2MB)
</a>
6. Tables
html
<!-- Bad: Layout table -->
<table>
<tr>
<td>Navigation</td>
<td>Content</td>
</tr>
</table>
<!-- Good: Data table -->
<table>
<caption>Monthly Expenses</caption>
<thead>
<tr>
<th scope="col">Category</th>
<th scope="col">January</th>
<th scope="col">February</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Food</th>
<td>$300</td>
<td>$280</td>
</tr>
<tr>
<th scope="row">Transport</th>
<td>$150</td>
<td>$160</td>
</tr>
</tbody>
</table>
<!-- Complex table with ARIA -->
<table aria-describedby="table-summary">
<caption>Project Timeline</caption>
<thead>
<tr>
<th id="task">Task</th>
<th id="owner">Owner</th>
<th id="status">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td headers="task">Design</td>
<td headers="owner">Alice</td>
<td headers="status">Completed</td>
</tr>
</tbody>
</table>
<p id="table-summary" class="sr-only">
Project tasks with owners and current status
</p>
Complete Accessible Examples
Example 1: Accessible Form
html
<form aria-labelledby="form-title" novalidate>
<h2 id="form-title">Contact Us</h2>
<!-- Name field -->
<div class="form-group">
<label for="full-name">
Full Name <span class="required">*</span>
</label>
<input type="text"
id="full-name"
name="fullName"
required
aria-required="true"
aria-describedby="name-hint">
<p id="name-hint" class="form-hint">
Enter your first and last name
</p>
<div id="name-error" role="alert" aria-live="polite"></div>
</div>
<!-- Email field -->
<div class="form-group">
<label for="email">Email Address</label>
<input type="email"
id="email"
name="email"
aria-describedby="email-hint">
<p id="email-hint" class="form-hint">
We'll never share your email
</p>
</div>
<!-- Radio buttons -->
<fieldset>
<legend>Preferred Contact Method</legend>
<div class="radio-group">
<input type="radio"
id="contact-email"
name="contactMethod"
value="email"
checked>
<label for="contact-email">Email</label>
</div>
<div class="radio-group">
<input type="radio"
id="contact-phone"
name="contactMethod"
value="phone">
<label for="contact-phone">Phone</label>
</div>
</fieldset>
<!-- Submit -->
<button type="submit" class="btn-primary">
Send Message
</button>
</form>
<script>
// Form validation with accessibility
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
const nameInput = document.getElementById('full-name');
const nameError = document.getElementById('name-error');
if (!nameInput.value.trim()) {
nameInput.setAttribute('aria-invalid', 'true');
nameError.textContent = 'Please enter your name';
nameInput.focus();
} else {
nameInput.setAttribute('aria-invalid', 'false');
nameError.textContent = '';
// Submit form
}
});
</script>
Example 2: Accessible Navigation
html
<header>
<!-- Skip link -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<!-- Logo -->
<a href="/" class="logo">
<img src="logo.svg" alt="Accessible Web Co.">
</a>
<!-- Main navigation -->
<nav aria-label="Primary">
<button class="menu-toggle"
aria-expanded="false"
aria-controls="main-menu"
aria-label="Toggle menu">
<span class="hamburger"></span>
Menu
</button>
<ul id="main-menu" class="nav-menu">
<li>
<a href="/" aria-current="page">Home</a>
</li>
<li>
<a href="/products" aria-haspopup="true">Products</a>
<ul class="dropdown" aria-label="Products submenu">
<li><a href="/products/web">Web Apps</a></li>
<li><a href="/products/mobile">Mobile Apps</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<!-- Search -->
<div role="search">
<label for="search" class="sr-only">Search site</label>
<input type="search" id="search" placeholder="Search...">
<button type="submit" aria-label="Search">
🔍
</button>
</div>
</header>
<script>
// Accessible mobile menu
const menuToggle = document.querySelector('.menu-toggle');
const mainMenu = document.getElementById('main-menu');
menuToggle.addEventListener('click', function() {
const isExpanded = this.getAttribute('aria-expanded') === 'true';
this.setAttribute('aria-expanded', !isExpanded);
mainMenu.classList.toggle('show');
// Focus management
if (!isExpanded) {
// When opening, focus first menu item
mainMenu.querySelector('a').focus();
} else {
// When closing, focus back to toggle
this.focus();
}
});
// Close menu on Escape
mainMenu.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
menuToggle.click();
}
});
</script>
Example 3: Accessible Modal Dialog
html
<button onclick="openModal('settings-modal')">
Open Settings
</button>
<div id="settings-modal"
class="modal"
role="dialog"
aria-labelledby="modal-title"
aria-modal="true"
aria-hidden="true">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Settings</h2>
<button class="close-button"
onclick="closeModal('settings-modal')"
aria-label="Close">
×
</button>
</div>
<div class="modal-body">
<form>
<!-- Settings form -->
</form>
</div>
<div class="modal-footer">
<button onclick="closeModal('settings-modal')">
Cancel
</button>
<button onclick="saveSettings()">
Save Changes
</button>
</div>
</div>
<div class="modal-overlay"
onclick="closeModal('settings-modal')"
tabindex="-1"></div>
</div>
<script>
let previousFocus;
function openModal(modalId) {
const modal = document.getElementById(modalId);
previousFocus = document.activeElement;
modal.setAttribute('aria-hidden', 'false');
modal.style.display = 'block';
// Trap focus inside modal
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
firstFocusable.focus();
// Handle tab key
modal.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
if (e.key === 'Escape') {
closeModal(modalId);
}
});
// Hide from screen readers outside modal
document.querySelectorAll('body > *:not(#settings-modal)')
.forEach(el => el.setAttribute('aria-hidden', 'true'));
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
modal.setAttribute('aria-hidden', 'true');
modal.style.display = 'none';
// Restore focus
if (previousFocus) {
previousFocus.focus();
}
// Restore screen reader access
document.querySelectorAll('body > *')
.forEach(el => el.setAttribute('aria-hidden', 'false'));
}
</script>
Testing Accessibility
Manual Testing:
html
<!-- Test with keyboard only --> <!-- 1. Tab through page - logical order? --> <!-- 2. All interactive elements focusable? --> <!-- 3. Visible focus indicators? --> <!-- Test with screen reader --> <!-- 1. Install NVDA (Windows) or VoiceOver (Mac) --> <!-- 2. Navigate page - makes sense? --> <!-- Visual inspection --> <!-- 1. Sufficient color contrast? --> <!-- 2. Text resizable without breaking layout? --> <!-- 3. Alternative text for images? -->
Automated Testing Tools:
html
<!-- Add testing script -->
<script src="https://cdn.jsdelivr.net/npm/axe-core@latest/axe.min.js"></script>
<script>
if (typeof axe !== 'undefined') {
axe.run(function(err, results) {
if (err) throw err;
console.log('Accessibility issues:', results.violations);
});
}
</script>
<!-- Browser extensions -->
<!--
1. axe DevTools (Chrome, Firefox, Edge)
2. Lighthouse (built into Chrome DevTools)
3. WAVE Evaluation Tool
-->
Quick Accessibility Checklist
Must Have:
html
✓ Semantic HTML elements ✓ Proper heading hierarchy (h1-h6) ✓ Alt text for images ✓ Form labels ✓ Keyboard navigation ✓ Sufficient color contrast ✓ Focus indicators
Should Have:
html
✓ ARIA attributes where needed ✓ Skip navigation link ✓ Error messages with aria-live ✓ Responsive design ✓ Captions for videos ✓ Transcripts for audio
Nice to Have:
html
✓ Reduced motion support ✓ Dark/light mode ✓ Language attributes ✓ Print styles ✓ Touch target sizes
Common Accessibility Mistakes
html
<!-- ❌ Bad practices --> <div onclick="submit()">Submit</div> <!-- Not keyboard accessible --> <img src="info.png"> <!-- Missing alt text --> <font size="4">Large text</font> <!-- Deprecated, use CSS --> <blink>Important!</blink> <!-- Never use blink --> <marquee>Scrolling text</marquee> <!-- Distracting --> <button style="outline: none;">No focus</button> <!-- Hides focus indicator --> <a href="#" style="color: #ccc;">Link</a> <!-- Low contrast --> <table> <tr><td>Layout</td></tr> </table> <!-- Table for layout --> <!-- ✅ Good practices --> <button onclick="submit()">Submit</button> <!-- Semantic button --> <img src="info.png" alt="Information icon"> <!-- Descriptive alt --> <p style="font-size: 1.2rem;">Large text</p> <!-- CSS for styling --> <strong>Important!</strong> <!-- Semantic emphasis --> <button style="outline: 2px solid blue;">Focusable</button> <a href="#" style="color: #0056b3;">Link</a> <!-- Good contrast --> <div class="layout-grid">...</div> <!-- CSS for layout -->
Resources and Tools
Testing Tools:
-
axe DevTools: Browser extension for automated testing
-
Lighthouse: Built into Chrome DevTools
-
WAVE: Web Accessibility Evaluation Tool
-
Color Contrast Analyzers: Check contrast ratios
-
Screen Readers: NVDA, JAWS, VoiceOver
Guidelines:
-
WCAG 2.1: Web Content Accessibility Guidelines
-
WAI-ARIA: Accessible Rich Internet Applications
-
Section 508: US government accessibility requirements
Remember:
-
Accessibility is not a feature - it's a requirement
-
Test early and often
-
Involve users with disabilities in testing
-
Accessibility benefits everyone
-
Start with semantic HTML - it solves 80% of issues
Accessibility isn't just about compliance - it's about creating a web that works for everyone, regardless of their abilities or how they access the internet.
Comments (0)
No comments yet. Be the first to comment!