13 min read
Part of Series: Web Fundamentals & Standards

Web Accessibility

Learn WCAG guidelines, semantic HTML, ARIA attributes, and screen reader optimization to create inclusive websites that work for everyone, including users with disabilities.

The Web Content Accessibility Guidelines (WCAG) 2.2, developed by the W3C, serve as the international standard for web accessibility. These guidelines are organized into a hierarchical structure with three compliance levels, each building upon the previous one. Level A (Essential Support) represents the minimum accessibility requirements. Without meeting these criteria, assistive technologies may not be able to read, understand, or operate your website. This level includes 35 success criteria covering fundamental accessibility barriers.

Level AA (Ideal Support) is the recommended standard for most websites and is required by many accessibility laws worldwide, including the ADA in the United States. This level includes an additional 28 success criteria and represents a balance between accessibility improvement and implementation feasibility.

Level AAA (Specialized Support) provides the highest level of accessibility with 23 additional success criteria. However, it’s not recommended as a blanket requirement for entire websites, as some content cannot meet all AAA criteria.

WCAG is built on four fundamental principles known as POUR:

Information and user interface components must be presentable to users in ways they can perceive. This means:

  • Providing text alternatives for non-text content
  • Offering captions and transcripts for multimedia
  • Ensuring sufficient color contrast
  • Making content adaptable to different presentations without losing meaning

User interface components and navigation must be operable by all users. Key requirements include:

  • Making all functionality available via keyboard
  • Providing users enough time to read content
  • Avoiding content that causes seizures or physical reactions
  • Helping users navigate and find content

Information and the operation of the user interface must be understandable. This involves:

  • Making text readable and understandable
  • Making content appear and operate predictably
  • Helping users avoid and correct mistakes

Content must be robust enough for interpretation by various assistive technologies. This requires:

  • Using valid, semantic markup
  • Ensuring compatibility with current and future assistive technologies

Semantic HTML Elements Use HTML5 semantic elements to provide meaning and structure to your content:

<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="#main">Skip to main content</a></li>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main id="main">
<article>
<h1>Page Title</h1>
<section>
<h2>Section Heading</h2>
<p>Content goes here...</p>
</section>
</article>
</main>
<aside>
<h2>Related Information</h2>
</aside>
<footer>
<p>&copy; 2024 Your Website</p>
</footer>

Heading Hierarchy Implement a logical heading structure without skipping levels:

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

Language Declaration Always specify the document language and mark language changes:

<html lang="en">
<head>
<title>English Page</title>
</head>
<body>
<p>This is English text.</p>
<p lang="es">Este texto está en español.</p>
</body>
</html>

Forms are critical interaction points that require careful accessibility implementation:

Proper Labeling Every form control must have an accessible label:

<!-- Explicit labeling (preferred) -->
<label for="email">Email Address (required)</label>
<input type="email" id="email" name="email" required aria-describedby="email-error" />
<!-- Implicit labeling -->
<label>
Password
<input type="password" name="password" required />
</label>
<!-- Using aria-label when visual label isn't desired -->
<input type="search" name="search" aria-label="Search products" placeholder="Search..." />

Grouping Related Controls Use fieldset and legend for radio buttons and checkboxes:

<fieldset>
<legend>Preferred Contact Method</legend>
<input type="radio" id="email" name="contact" value="email" />
<label for="email">Email</label>
<input type="radio" id="phone" name="contact" value="phone" />
<label for="phone">Phone</label>
<input type="radio" id="mail" name="contact" value="mail" />
<label for="mail">Mail</label>
</fieldset>

Error Handling and Validation Provide clear, helpful error messages:

<label for="username">Username (required)</label>
<input type="text" id="username" name="username" required aria-describedby="username-error" aria-invalid="true" />
<div id="username-error" role="alert">Username is required and must be at least 3 characters long.</div>

Instructions and Help Text Use aria-describedby to associate help text with form controls:

<label for="password">Password</label>
<input type="password" id="password" name="password" aria-describedby="password-help" required />
<div id="password-help">Password must be at least 8 characters long and contain at least one number.</div>

Alternative Text for Images Provide meaningful alt text that serves the same purpose as the image:

<!-- Informative image -->
<img src="sales-chart.png" alt="Sales increased 25% from January to March 2024" />
<!-- Decorative image -->
<img src="decorative-border.png" alt="" role="presentation" />
<!-- Functional image (button) -->
<button type="submit">
<img src="search-icon.png" alt="Search" />
</button>
<!-- Complex image with longer description -->
<img src="complex-chart.png" alt="Quarterly sales data" aria-describedby="chart-desc" />
<div id="chart-desc">
Detailed description: Sales data shows Q1 at $100k, Q2 at $150k, Q3 at $175k, and Q4 at $200k, representing steady
growth throughout the year.
</div>

Video and Audio Accessibility Multimedia content requires multiple accessibility features:

<!-- Video with captions and audio description -->
<video controls>
<source src="training-video.mp4" type="video/mp4" />
<track kind="captions" src="captions.vtt" srclang="en" label="English captions" />
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Audio descriptions" />
<p>Your browser doesn't support video. <a href="transcript.html">Read the transcript</a></p>
</video>
<!-- Audio-only content -->
<audio controls>
<source src="podcast.mp3" type="audio/mpeg" />
<p>Your browser doesn't support audio. <a href="transcript.html">Read the transcript</a></p>
</audio>
<p><a href="podcast-transcript.txt">Download transcript</a></p>

Buttons and Links Ensure interactive elements have clear purposes and are keyboard accessible:

<!-- Descriptive button text -->
<button type="submit">Submit Contact Form</button>
<!-- Button with icon needs accessible text -->
<button type="button" aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Link with clear destination -->
<a href="/products/laptops">View all laptop models</a>
<!-- Link opening new window/tab -->
<a href="/terms.pdf" target="_blank"> Terms of Service <span class="sr-only">(opens in new tab)</span> </a>

Custom Interactive Components When creating custom widgets, use ARIA roles, properties, and states:

<!-- Custom dropdown menu -->
<div class="dropdown">
<button aria-haspopup="true" aria-expanded="false" id="menu-button">Options</button>
<ul role="menu" aria-labelledby="menu-button" hidden>
<li role="menuitem"><a href="/option1">Option 1</a></li>
<li role="menuitem"><a href="/option2">Option 2</a></li>
<li role="menuitem"><a href="/option3">Option 3</a></li>
</ul>
</div>
<!-- Custom tab interface -->
<div role="tablist" aria-label="Content sections">
<button role="tab" aria-selected="true" aria-controls="panel1" id="tab1">Section 1</button>
<button role="tab" aria-selected="false" aria-controls="panel2" id="tab2">Section 2</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="tab1">
<h2>Section 1 Content</h2>
<p>Content for the first section...</p>
</div>
<div role="tabpanel" id="panel2" aria-labelledby="tab2" hidden>
<h2>Section 2 Content</h2>
<p>Content for the second section...</p>
</div>

Color Contrast Requirements Ensure sufficient contrast ratios for all text and UI components:

  • Normal text: Minimum 4.5:1 contrast ratio (WCAG AA)
  • Large text (18pt+ or 14pt+ bold): Minimum 3:1 contrast ratio
  • UI components: Minimum 3:1 contrast ratio for borders, icons, and focus indicators

Color-Independent Information Never rely solely on color to convey information:

<!-- Bad: Only color indicates required field -->
<label style="color: red;">Email</label>
<input type="email" name="email" />
<!-- Good: Color plus text/symbol indicator -->
<label>Email <span class="required" aria-label="required">*</span></label>
<input type="email" name="email" required />
<!-- Good: Error states with multiple indicators -->
<label for="email">Email</label>
<input type="email" id="email" name="email" aria-invalid="true" class="error" aria-describedby="email-error" />
<div id="email-error" class="error-message" role="alert">⚠️ Please enter a valid email address</div>

Focus Indicators Provide clear, visible focus indicators for all interactive elements:

/* Ensure focus indicators are visible */
button:focus,
input:focus,
select:focus,
textarea:focus,
a:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* Custom focus styles that meet contrast requirements */
.custom-button:focus {
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
outline: 2px solid #005fcc;
}

Tab Order and Keyboard Traps Ensure logical tab order and prevent keyboard traps:

<!-- Use tabindex sparingly and appropriately -->
<div tabindex="-1" id="error-summary">
<!-- Focusable via JavaScript, not in tab order -->
</div>
<button tabindex="0">Normal tab order</button>
<!-- JavaScript for managing focus -->
<script>
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
const firstElement = focusableElements[0]
const lastElement = focusableElements[focusableElements.length - 1]
element.addEventListener("keydown", function (e) {
if (e.key === "Tab") {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus()
e.preventDefault()
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus()
e.preventDefault()
}
}
}
})
}
</script>

Live Regions for Dynamic Updates Use ARIA live regions to announce dynamic content changes:

<!-- Status messages -->
<div id="status" role="status" aria-live="polite"></div>
<!-- Alert messages -->
<div id="alerts" role="alert" aria-live="assertive"></div>
<!-- JavaScript to update live regions -->
<script>
function announceStatus(message) {
const statusElement = document.getElementById("status")
statusElement.textContent = message
}
function announceAlert(message) {
const alertElement = document.getElementById("alerts")
alertElement.textContent = message
}
// Example usage
announceStatus("Form saved successfully")
announceAlert("Connection lost. Please check your internet connection.")
</script>

Focus Management in SPAs Manage focus appropriately when content changes dynamically:

// Focus management for route changes
function navigateToPage(pageContent, pageTitle) {
// Update page content
document.getElementById("main-content").innerHTML = pageContent
// Update page title
document.title = pageTitle
// Move focus to main content area
const mainContent = document.getElementById("main-content")
mainContent.setAttribute("tabindex", "-1")
mainContent.focus()
// Announce page change to screen readers
announceStatus(`Navigated to ${pageTitle}`)
}
// Modal dialog focus management
function openModal(modalElement) {
// Store currently focused element
const previouslyFocused = document.activeElement
// Show modal
modalElement.style.display = "block"
modalElement.setAttribute("aria-hidden", "false")
// Move focus to first focusable element in modal
const firstFocusable = modalElement.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
)
if (firstFocusable) firstFocusable.focus()
// Trap focus within modal
trapFocus(modalElement)
// Return focus when modal closes
modalElement.addEventListener("close", function () {
previouslyFocused.focus()
})
}

Effective accessibility testing requires a combination of automated tools, manual testing, and user testing with assistive technologies.

axe-core is the most widely used accessibility testing library, powering many other tools. It provides comprehensive coverage with minimal false positives and integrates with most testing frameworks.

Lighthouse by Google offers built-in accessibility audits alongside performance and SEO checks. It’s available in Chrome DevTools and as a CI/CD tool.

WAVE (Web Accessibility Evaluation Tool) provides visual feedback directly on web pages, making it easy to identify and understand accessibility issues.

Pa11y is a command-line tool perfect for automated testing and CI/CD integration. It can test individual pages or entire sitemaps.

  • axe DevTools: Browser extension for interactive accessibility testing
  • Accessibility Insights: Microsoft’s comprehensive accessibility testing platform
  • Colour Contrast Analyser: Dedicated tool for testing color contrast ratios
  • Accessibility Developer Tools: Chrome extension for accessibility auditing

Testing with actual screen readers is crucial for ensuring real-world accessibility.

  • NVDA (Windows): Free, open-source screen reader
  • JAWS (Windows): Popular commercial screen reader
  • VoiceOver (macOS/iOS): Built-in Apple screen reader
  • TalkBack (Android): Built-in Android screen reader
  1. Automated Testing: Run automated scans to catch obvious issues
  2. Manual Testing: Test keyboard navigation, screen reader compatibility, and complex interactions
  3. User Testing: Include users with disabilities in your testing process
  4. Continuous Testing: Integrate accessibility testing into your development workflow

Integrating accessibility testing into your continuous integration and deployment pipeline ensures that accessibility issues are caught early and consistently.

GitHub Actions Example:

name: Accessibility Testing
on: [push, pull_request]
jobs:
accessibility-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Install dependencies
run: npm install
- name: Build application
run: npm run build
- name: Start application
run: npm start &
- name: Wait for application to start
run: sleep 30
- name: Run Pa11y tests
run: |
npx pa11y-ci --sitemap http://localhost:3000/sitemap.xml
- name: Run axe tests with Cypress
run: npx cypress run --spec "cypress/integration/accessibility.spec.js"

Cypress with axe-core:

cypress/integration/accessibility.spec.js
describe("Accessibility Tests", () => {
beforeEach(() => {
cy.visit("/")
cy.injectAxe()
})
it("Has no accessibility violations on home page", () => {
cy.checkA11y()
})
it("Has no accessibility violations on contact form", () => {
cy.visit("/contact")
cy.checkA11y()
})
it("Has no accessibility violations after form interaction", () => {
cy.visit("/contact")
cy.get("#name").type("Test User")
cy.get("#email").type("test@example.com")
cy.checkA11y()
})
})

Playwright with axe-core:

const { test, expect } = require("@playwright/test")
const AxeBuilder = require("@axe-core/playwright")
test("Homepage accessibility", async ({ page }) => {
await page.goto("/")
const accessibilityScanResults = await new AxeBuilder({ page }).analyze()
expect(accessibilityScanResults.violations).toEqual([])
})

Implement accessibility quality gates that fail builds when critical issues are found:

.github/workflows/accessibility.yml
- name: Run accessibility tests
run: |
npx pa11y-ci --threshold 5 http://localhost:3000
continue-on-error: false
- name: Generate accessibility report
run: |
npx pa11y-ci --reporter json > accessibility-report.json
- name: Upload accessibility report
uses: actions/upload-artifact@v2
with:
name: accessibility-report
path: accessibility-report.json

This comprehensive checklist covers all major aspects of web accessibility, organized by component and priority level. Each item includes the corresponding WCAG success criteria, testing methods, and recommended tools.

  1. Priority-Based Implementation: Start with “High” priority items that address the most critical accessibility barriers
  2. Component-Based Review: Use the category organization to systematically review each part of your website
  3. WCAG Level Targeting: Focus on Level A and AA items for legal compliance and broad accessibility
  4. Testing Integration: Use the specified testing methods and tools to verify implementation
  5. Regular Audits: Review the checklist regularly, especially when adding new features or components
  • Structure & Semantics: Proper HTML structure and semantic markup
  • Images & Media: Alternative text, captions, and multimedia accessibility
  • Color & Contrast: Visual accessibility and color-independent design
  • Keyboard Navigation: Full keyboard accessibility and focus management
  • Forms: Proper labeling, instructions, and error handling
  • Interactive Elements: Buttons, links, and custom components
  • Dynamic Content: Live regions and focus management for SPAs
  • Mobile & Responsive: Touch targets and responsive accessibility
  • Navigation & Links: Clear navigation and descriptive link text
  • Tables: Proper table structure and labeling

When building web components, accessibility requires special consideration:

class AccessibleButton extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: "open" })
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
button {
padding: 12px 24px;
border: 2px solid #333;
background: #fff;
cursor: pointer;
}
button:focus {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
button:hover {
background: #f0f0f0;
}
</style>
<button part="button">
<slot></slot>
</button>
`
// Ensure button receives proper focus
const button = this.shadowRoot.querySelector("button")
button.addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("button-click", {
bubbles: true,
composed: true,
}),
)
})
// Forward ARIA attributes
if (this.hasAttribute("aria-label")) {
button.setAttribute("aria-label", this.getAttribute("aria-label"))
}
}
}
customElements.define("accessible-button", AccessibleButton)

Accessibility features should not compromise performance:

  • Lazy load non-critical accessibility features
  • Optimize screen reader announcements to avoid spam
  • Use efficient selectors in accessibility testing
  • Minimize DOM manipulations for focus management

Consider accessibility across different languages and cultures:

<!-- Proper language tagging for mixed-language content -->
<html lang="en">
<head>
<title>Multilingual Accessibility Example</title>
</head>
<body>
<h1>Welcome to Our Site</h1>
<p>This content is in English.</p>
<blockquote lang="es">
<p>Este contenido está en español.</p>
</blockquote>
<p lang="ar" dir="rtl">هذا المحتوى باللغة العربية</p>
</body>
</html>
  1. Design with Accessibility in Mind: Consider accessibility from the design phase, not as an afterthought
  2. Use Progressive Enhancement: Build core functionality that works without JavaScript, then enhance
  3. Test Early and Often: Integrate accessibility testing throughout the development process
  4. Learn from Real Users: Include users with disabilities in your user testing
  5. Stay Updated: Keep up with WCAG updates and accessibility best practices
  6. Document Accessibility Features: Maintain documentation of accessibility implementations for your team

Web accessibility is not just a technical requirement but also a legal necessity in many jurisdictions. The Americans with Disabilities Act (ADA), European Accessibility Act, and similar laws worldwide require digital accessibility. Beyond compliance, accessible websites provide business benefits including:

  • Expanded market reach (15% of the global population has some form of disability)
  • Improved SEO performance
  • Better overall usability for all users
  • Enhanced brand reputation and social responsibility

As web technologies evolve, accessibility must evolve with them. Emerging areas include:

  • AI and Machine Learning: Tools for automated accessibility testing and content generation
  • Voice Interfaces: Accessibility considerations for voice-controlled applications
  • Augmented/Virtual Reality: New accessibility challenges and opportunities in immersive experiences
  • IoT and Smart Devices: Accessibility in connected device interfaces

Implementing web accessibility requires a systematic approach combining technical knowledge, proper tooling, and user empathy. Use this guide as your comprehensive reference, but remember that accessibility is an ongoing journey, not a destination. Regular testing, user feedback, and continuous learning are essential for maintaining and improving the accessibility of your web applications.

By following the guidelines, using the tools, and implementing the checklist provided in this guide, you’ll be well-equipped to create web experiences that are truly accessible to all users. Start with the high-priority items, establish automated testing in your CI/CD pipeline, and gradually work toward comprehensive accessibility coverage across all components of your website.

Remember: accessible design is good design, and the techniques that help users with disabilities often improve the experience for everyone.

Tags

Read more

  • Previous in series: Web Fundamentals & Standards

    Image Formats for Web Performance

    5 min read

    Master modern image formats including JPEG, WebP, AVIF, and PNG, understanding compression algorithms, color spaces, HDR support, and optimal deployment strategies.

  • Next in series: Web Fundamentals & Standards

    Web Security Guide

    41 min read

    Master web application security from OWASP Top 10 vulnerabilities to production implementation, covering authentication, authorization, input validation, and security headers for building secure applications.