Menu

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:

  1. Accessibility is not a feature - it's a requirement

  2. Test early and often

  3. Involve users with disabilities in testing

  4. Accessibility benefits everyone

  5. 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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?