What is Web Accessibility?

Web accessibility ensures that websites and applications are usable by people with disabilities, including visual, auditory, motor, and cognitive impairments. The Web Content Accessibility Guidelines (WCAG) provide the framework for creating accessible web content.

WCAG Principles: POUR

1. Perceivable

Information must be presentable in ways users can perceive.

2. Operable

Interface components must be operable by all users.

3. Understandable

Information and UI operations must be understandable.

4. Robust

Content must work with various assistive technologies.

Semantic HTML Foundation

Using proper HTML elements provides built-in accessibility features.

Headings Structure



html

<h1>Main Page Title</h1>
  <h2>Section Heading</h2>
    <h3>Subsection Heading</h3>
    <h3>Another Subsection</h3>
  <h2>Another Section</h2>

Rules:

  • Use only one <h1> per page
  • Don't skip heading levels
  • Use headings for structure, not styling

Meaningful Links



html

<!-- Bad -->
<a href="article.html">Click here</a>
<a href="download.pdf">Read more</a>

<!-- Good -->
<a href="article.html">Read our guide to web accessibility</a>
<a href="annual-report.pdf">Download 2024 Annual Report (PDF, 2MB)</a>

Form Labels and Structure



html

<form>
    <fieldset>
        <legend>Contact Information</legend>
        
        <label for="fullname">Full Name *</label>
        <input type="text" id="fullname" name="fullname" required 
               aria-describedby="name-help">
        <div id="name-help">Enter your first and last name</div>
        
        <label for="email">Email Address *</label>
        <input type="email" id="email" name="email" required 
               aria-describedby="email-error">
        <div id="email-error" role="alert" aria-live="polite"></div>
    </fieldset>
</form>

ARIA (Accessible Rich Internet Applications)

ARIA attributes enhance HTML accessibility when semantic HTML isn't sufficient.

Essential ARIA Attributes

aria-label

Provides accessible name when visible text isn't descriptive:



html

<button aria-label="Close dialog">×</button>
<input type="search" aria-label="Search products">
aria-describedby

References elements that describe the current element:



html

<input type="password" aria-describedby="pwd-help">
<div id="pwd-help">Password must be at least 8 characters</div>
aria-expanded

Indicates if a collapsible element is expanded:



html

<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
</ul>

<script>
document.querySelector('button').addEventListener('click', function() {
    const menu = document.getElementById('menu');
    const isExpanded = this.getAttribute('aria-expanded') === 'true';
    
    this.setAttribute('aria-expanded', !isExpanded);
    menu.hidden = isExpanded;
});
</script>

ARIA Roles

Define what an element is or does:



html

<div role="button" tabindex="0">Custom Button</div>
<div role="alert">Error: Please fill required fields</div>
<nav role="navigation" aria-label="Main navigation"></nav>
<div role="tablist">
    <button role="tab" aria-selected="true">Tab 1</button>
    <button role="tab" aria-selected="false">Tab 2</button>
</div>

Keyboard Navigation

Ensure all interactive elements are keyboard accessible.

Focus Management



html

<style>
.focus-visible {
    outline: 2px solid #005fcc;
    outline-offset: 2px;
}

/* Hide focus ring for mouse users */
.no-focus-visible {
    outline: none;
}
</style>

<button class="btn">Accessible Button</button>

<script>
// Enhanced focus management
document.addEventListener('keydown', function(e) {
    if (e.key === 'Tab') {
        document.body.classList.add('using-keyboard');
    }
});

document.addEventListener('mousedown', function() {
    document.body.classList.remove('using-keyboard');
});
</script>

Skip Links

Allow keyboard users to skip to main content:



html

<body>
    <a href="#main-content" class="skip-link">Skip to main content</a>
    
    <nav><!-- Navigation --></nav>
    
    <main id="main-content">
        <h1>Main Content</h1>
        <!-- Page content -->
    </main>
</body>

<style>
.skip-link {
    position: absolute;
    top: -40px;
    left: 6px;
    background: #000;
    color: #fff;
    padding: 8px;
    text-decoration: none;
    border-radius: 0 0 4px 4px;
}

.skip-link:focus {
    top: 0;
}
</style>

Images and Media Accessibility

Alt Text Best Practices



html

<!-- Decorative images -->
<img src="decorative-border.png" alt="" role="presentation">

<!-- Informative images -->
<img src="chart.png" alt="Sales increased 25% from Q1 to Q2 2024">

<!-- Functional images -->
<img src="search-icon.png" alt="Search">

<!-- Complex images -->
<img src="complex-chart.png" alt="Quarterly sales data" 
     aria-describedby="chart-details">
<div id="chart-details">
    <p>Detailed description of the chart data...</p>
</div>

Video Accessibility



html

<video controls>
    <source src="video.mp4" type="video/mp4">
    <track kind="captions" src="captions.vtt" srclang="en" label="English">
    <track kind="descriptions" src="descriptions.vtt" srclang="en" 
           label="Audio descriptions">
    Your browser doesn't support video.
</video>

Color and Contrast

Ensure sufficient color contrast and don't rely solely on color.



html

<!-- Bad: Only color indicates status -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

<!-- Good: Icon + color + text -->
<span style="color: red;">
    <span aria-hidden="true">❌</span> Error: Invalid email format
</span>
<span style="color: green;">
    <span aria-hidden="true">✅</span> Success: Account created
</span>

Complete Accessible Component Example

Here's an accessible modal dialog:



html

<button id="open-modal">Open Settings</button>

<div id="modal" class="modal" role="dialog" aria-modal="true" 
     aria-labelledby="modal-title" hidden>
    <div class="modal-content">
        <header>
            <h2 id="modal-title">Settings</h2>
            <button id="close-modal" aria-label="Close settings dialog">×</button>
        </header>
        
        <main>
            <form>
                <fieldset>
                    <legend>Notification Preferences</legend>
                    
                    <label>
                        <input type="checkbox" name="email-notifications">
                        Email notifications
                    </label>
                    
                    <label>
                        <input type="checkbox" name="sms-notifications">
                        SMS notifications
                    </label>
                </fieldset>
                
                <button type="submit">Save Settings</button>
            </form>
        </main>
    </div>
</div>

<style>
.modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
}

.modal[hidden] {
    display: none;
}

.modal-content {
    background: white;
    padding: 20px;
    border-radius: 4px;
    max-width: 500px;
    width: 90%;
}
</style>

<script>
class AccessibleModal {
    constructor(modalId) {
        this.modal = document.getElementById(modalId);
        this.openBtn = document.getElementById('open-modal');
        this.closeBtn = document.getElementById('close-modal');
        this.focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
        
        this.init();
    }
    
    init() {
        this.openBtn.addEventListener('click', () => this.open());
        this.closeBtn.addEventListener('click', () => this.close());
        document.addEventListener('keydown', (e) => this.handleKeydown(e));
    }
    
    open() {
        this.lastFocusedElement = document.activeElement;
        this.modal.hidden = false;
        this.closeBtn.focus();
        document.body.style.overflow = 'hidden';
    }
    
    close() {
        this.modal.hidden = true;
        document.body.style.overflow = '';
        this.lastFocusedElement.focus();
    }
    
    handleKeydown(e) {
        if (!this.modal.hidden) {
            if (e.key === 'Escape') {
                this.close();
            }
            
            if (e.key === 'Tab') {
                this.trapFocus(e);
            }
        }
    }
    
    trapFocus(e) {
        const focusable = this.modal.querySelectorAll(this.focusableElements);
        const firstFocusable = focusable[0];
        const lastFocusable = focusable[focusable.length - 1];
        
        if (e.shiftKey) {
            if (document.activeElement === firstFocusable) {
                lastFocusable.focus();
                e.preventDefault();
            }
        } else {
            if (document.activeElement === lastFocusable) {
                firstFocusable.focus();
                e.preventDefault();
            }
        }
    }
}

new AccessibleModal('modal');
</script>

Testing for Accessibility

Automated Testing Tools:

  1. axe DevTools browser extension
  2. WAVE Web Accessibility Evaluator
  3. Lighthouse Accessibility Audit
  4. Pa11y command-line tool

Manual Testing:

  1. Navigate using only keyboard (Tab, Enter, Arrow keys)
  2. Test with screen reader (NVDA, JAWS, VoiceOver)
  3. Check color contrast ratios
  4. Verify zoom functionality (up to 200%)