Skip to main content
โšก Calmops

Web Accessibility (a11y): Complete Guide

Web accessibility ensures that people with disabilities can use your website. This guide covers everything you need to build accessible applications.

Semantic HTML

Structure Elements

<!-- Header -->
<header>
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
</header>

<!-- Main content -->
<main>
  <article>
    <h1>Article Title</h1>
    <p>Article content...</p>
  </article>
  
  <aside>
    <h2>Related Articles</h2>
  </aside>
</main>

<!-- Footer -->
<footer>
  <p>&copy; 2026 Company</p>
</footer>

Headings

<!-- Correct heading hierarchy -->
<h1>Page Title</h1>
  <h2>Section 1</h2>
    <h3>Subsection 1.1</h3>
    <h3>Subsection 1.2</h3>
  <h2>Section 2</h2>
    <h3>Subsection 2.1</h3>

<!-- DON'T skip levels -->
<!-- Bad -->
<h1>Title</h1>
<h3>Subtitle</h3>

<!-- Good -->
<h1>Title</h1>
<h2>Subtitle</h2>
<!-- Button - performs action -->
<button onclick="submitForm()">Submit</button>
<button onclick="openModal()">Open</button>
<button onclick="toggleMenu()">Menu</button>

<!-- Link - navigates -->
<a href="/page">Go to page</a>
<a href="/download">Download file</a>
<a href="#section">Jump to section</a>

ARIA Attributes

Role Attributes

<!-- Navigation role -->
<nav role="navigation" aria-label="Main">

<!-- Banner role (header) -->
<header role="banner">

<!-- Main role -->
<main role="main">

<!-- Contentinfo role (footer) -->
<footer role="contentinfo">

<!-- Complementary role -->
<aside role="complementary">

<!-- Search role -->
<form role="search">

ARIA Labels

<!-- aria-label -->
<button aria-label="Close menu">
  <svg>...</svg>
</button>

<!-- aria-labelledby -->
<div aria-labelledby="modal-title">
  <h2 id="modal-title">Confirm Action</h2>
</div>

<!-- aria-describedby -->
<input 
  type="email" 
  aria-describedby="email-help"
>
<span id="email-help">We'll never share your email</span>

ARIA States

<!-- Expanded -->
<button aria-expanded="false" aria-controls="menu">
  Menu
</button>
<ul id="menu" hidden>
  <li>Item 1</li>
</ul>

<!-- Selected -->
<tablist>
  <tab aria-selected="true">Tab 1</tab>
  <tab aria-selected="false">Tab 2</tab>
</tablist>

<!-- Checked/Radio -->
<input type="radio" aria-checked="true">
<input type="checkbox" aria-checked="true">

<!-- Disabled -->
<button disabled aria-disabled="true">

Live Regions

<!-- Polite - announced when idle -->
<div aria-live="polite" aria-atomic="true">
  <p>Changes saved</p>
</div>

<!-- Assertive - announced immediately -->
<div role="alert" aria-live="assertive">
  <p>Error: Please correct the form</p>
</div>

<!-- Status -->
<div role="status" aria-live="polite">
  <p>Ready</p>
</div>

Keyboard Navigation

Focus Management

/* Visible focus indicator */
:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

/* Remove default focus (bad practice) */
/* DON'T do this */
/*:focus {
  outline: none;
}*/

Tab Order

<!-- Logical tab order -->
<input tabindex="1">
<input tabindex="2">
<input tabindex="3">

<!-- Avoid positive tabindex -->
<!-- DON'T -->
<input tabindex="5">
<input tabindex="1">
<input tabindex="10">

<!-- Use 0 for natural order -->
<input tabindex="0"> <!-- included in natural order -->
<input tabindex="-1"> <!-- programmatically focusable only -->
<!-- Skip to main content -->
<a href="#main-content" class="skip-link">
  Skip to main content
</a>

<style>
.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: blue;
  color: white;
  padding: 8px;
  z-index: 100;
}

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

<main id="main-content">

Keyboard Handlers

// Handle keyboard events
function handleKeyDown(event) {
  switch (event.key) {
    case 'Enter':
    case ' ':
      // Activate button
      event.preventDefault();
      selectItem();
      break;
    case 'ArrowDown':
      event.preventDefault();
      moveFocus(1);
      break;
    case 'ArrowUp':
      event.preventDefault();
      moveFocus(-1);
      break;
    case 'Escape':
      closeModal();
      break;
    case 'Home':
      event.preventDefault();
      focusFirstItem();
      break;
    case 'End':
      event.preventDefault();
      focusLastItem();
      break;
  }
}

Forms

Labels

<!-- Explicit label -->
<label for="email">Email</label>
<input id="email" type="email">

<!-- Implicit label (wrapped) -->
<label>
  Email
  <input type="email">
</label>

<!-- Best - both -->
<label for="email">
  Email
  <span class="sr-only">(required)</span>
</label>
<input id="email" type="email" required aria-required="true">

Error Handling

<!-- Associate error with input -->
<label for="password">Password</label>
<input 
  id="password" 
  type="password" 
  aria-invalid="true"
  aria-describedby="password-error"
>
<span id="password-error" role="alert">
  Password must be at least 8 characters
</span>

<!-- Success state -->
<input 
  type="email" 
  aria-invalid="false"
  aria-describedby="email-success"
>
<span id="email-success" role="status">
  โœ“ Valid email
</span>

Images

<!-- Decorative -->
<img src="decoration.png" alt="">

<!-- Informative -->
<img src="chart.png" alt="Sales chart showing 50% growth">

<!-- Complex - use long description -->
<img 
  src="chart.png" 
  alt="Sales trend"
  aria-describedby="chart-desc"
>
<div id="chart-desc" hidden>
  <!-- Long description -->
</div>

Testing

Automated Testing

// eslint-plugin-jsx-a11y
// .eslintrc
{
  "plugins": ["jsx-a11y"],
  "rules": {
    "jsx-a11y/alt-text": "error",
    "jsx-a11y/aria-props": "error",
    "jsx-a11y/heading-has-content": "error",
    "jsx-a11y/label-has-associated-control": "error"
  }
}

// Axe-core
import axe from 'axe-core';

axe.run(document).then(results => {
  console.log(results.violations);
});

// In tests
import { toHaveNoViolations } from 'jest-axe';

expect(element).toHaveNoViolations();

Manual Testing Checklist

1. Keyboard Only
   - Can you navigate all interactive elements?
   - Can you activate all buttons?
   - Can you close modals with Escape?
   - Is focus visible everywhere?

2. Screen Reader
   - Are all images described?
   - Are forms properly labeled?
   - Are error messages announced?
   - Is reading order logical?

3. Zoom
   - Does layout work at 200% zoom?
   - Is no content cut off?
   - Are scrollbars available?

4. Color Contrast
   - Do text and backgrounds have 4.5:1 ratio?
   - Are focus indicators visible?
   - Are links distinguishable?

Browser DevTools

// Accessibility panel in DevTools
// Shows accessibility tree
// Highlights accessibility issues

// Lighthouse accessibility audit
// Run in Chrome DevTools > Lighthouse
// Check: "Accessibility"

WCAG Guidelines

Level A (Minimum)

  • Non-text content has alt text
  • Info and relationships are programmatically determined
  • Content can be presented in different ways
  • Info and user components must be distinguishable
  • Keyboard accessible
  • No keyboard traps
  • Page titles are descriptive
  • Focus order is logical

Level AA (Standard)

  • Contrast ratio 4.5:1 for text
  • Text resizable to 200%
  • Multiple ways to find pages
  • Focus visible
  • Consistent navigation
  • Consistent identification

Level AAA (Enhanced)

  • Contrast ratio 7:1 for text
  • No timing for reading
  • No content that could cause seizures
  • Multiple ways to locate pages
  • Focus not obscured

Color & Contrast

/* Minimum contrast (AA - 4.5:1) */
.text-primary {
  color: #1a1a1a; /* on white */
  background: #ffffff;
}

/* Enhanced contrast (AAA - 7:1) */
.text-enhanced {
  color: #0d0d0d; /* on white */
  background: #ffffff;
}

/* Large text (3:1) */
.text-large {
  font-size: 18px; /* or 14px bold */
  color: #4a4a4a;
  background: #ffffff;
}

/* Focus indicators */
button:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

Screen Reader Testing

Common Screen Readers

  • NVDA (Windows, free)
  • JAWS (Windows, paid)
  • VoiceOver (Mac/iOS, built-in)
  • TalkBack (Android, built-in)

What to Test

  1. Reading order - Logical flow when navigating
  2. Alt text - Meaningful descriptions
  3. Form labels - Properly associated
  4. Headings - Correct hierarchy
  5. Links - Descriptive text
  6. Dynamic content - Announced with live regions

Summary

Key accessibility principles:

  1. Semantic HTML

    • Use proper elements
    • Correct heading hierarchy
    • Buttons vs links
  2. ARIA

    • Use when needed
    • Don’t over-use
    • Follow spec
  3. Keyboard

    • All functionality accessible
    • Visible focus
    • Skip links
  4. Forms

    • Labels always present
    • Error association
    • Clear instructions
  5. Testing

    • Automated tools
    • Manual testing
    • Real screen readers

Accessibility benefits everyone - implement from the start!

Comments