Why Accessibility Matters

Web accessibility (often abbreviated as a11y) ensures that websites and applications are designed and developed so that people with disabilities can use them. Beyond being a legal requirement in many jurisdictions, accessibility is a fundamental aspect of inclusive design that benefits all users.
Accessibility isn’t just about accommodating users with permanent disabilities. It also helps users with temporary limitations (like a broken arm) or situational constraints (like bright sunlight on a screen).

Understanding Disabilities and Assistive Technologies

To build accessible websites, it’s important to understand different types of disabilities and the assistive technologies people use.

Visual Impairments

Includes blindness, low vision, and color blindness.Assistive Technologies:
  • Screen readers (JAWS, NVDA, VoiceOver)
  • Screen magnifiers
  • Braille displays
  • High contrast modes

Auditory Impairments

Includes deafness and hard of hearing.Assistive Technologies:
  • Captions and transcripts
  • Visual alternatives for audio cues

Motor Impairments

Includes limited fine motor control, tremors, and paralysis.Assistive Technologies:
  • Keyboard-only navigation
  • Switch devices
  • Voice recognition
  • Eye tracking

Cognitive Impairments

Includes learning disabilities, attention deficits, and memory issues.Assistive Technologies:
  • Simple layouts
  • Reading tools
  • Text-to-speech
  • Predictive text

Accessibility Standards and Guidelines

Web Content Accessibility Guidelines (WCAG)

WCAG is the most widely recognized set of accessibility guidelines, organized around four principles:
WCAG defines three levels of conformance:
  • Level A: Minimum level of accessibility (must satisfy)
  • Level AA: Addresses the major barriers (commonly required by regulations)
  • Level AAA: Highest level of accessibility (ideal goal)

Accessibility Regulations

Many countries have laws requiring digital accessibility:
  • United States: Americans with Disabilities Act (ADA) and Section 508
  • European Union: European Accessibility Act and EN 301 549
  • United Kingdom: Equality Act 2010
  • Canada: Accessible Canada Act
  • Australia: Disability Discrimination Act

HTML Accessibility Techniques

Semantic HTML

Using semantic HTML elements is one of the most important accessibility practices.
<!-- Bad: Divs with no semantic meaning -->
<div class="header">
  <div class="nav">
    <div class="nav-item">Home</div>
  </div>
</div>
<div class="main">
  <div class="section">
    <div class="heading">Welcome</div>
    <div class="text">Content goes here</div>
  </div>
</div>

<!-- Good: Semantic HTML -->
<header>
  <nav>
    <ul>
      <li><a href="/">Home</a></li>
    </ul>
  </nav>
</header>
<main>
  <section>
    <h1>Welcome</h1>
    <p>Content goes here</p>
  </section>
</main>

Accessible Forms

Forms are often challenging for users with disabilities. Here’s how to make them accessible:
<!-- Bad: Form without proper labels and structure -->
<form>
  Name: <input type="text" />
  <span class="error">Required field</span>

  <div><input type="checkbox" /> Subscribe to newsletter</div>

  <button>Submit</button>
</form>

<!-- Good: Accessible form -->
<form>
  <div>
    <label for="name">Name</label>
    <input
      id="name"
      type="text"
      aria-required="true"
      aria-describedby="name-error"
    />
    <p id="name-error" class="error" role="alert">Required field</p>
  </div>

  <div>
    <input id="newsletter" type="checkbox" />
    <label for="newsletter">Subscribe to newsletter</label>
  </div>

  <button type="submit">Submit</button>
</form>

ARIA (Accessible Rich Internet Applications)

ARIA attributes enhance accessibility when HTML alone isn’t sufficient.
<!-- Using ARIA roles -->
<div role="alert">Your form has been submitted successfully.</div>

<!-- Using ARIA states -->
<button aria-expanded="false" aria-controls="dropdown-menu">Menu</button>
<ul id="dropdown-menu" hidden>
  <li><a href="#">Option 1</a></li>
  <li><a href="#">Option 2</a></li>
</ul>

<!-- Using ARIA properties -->
<div id="tooltip" role="tooltip">Additional information</div>
<button aria-describedby="tooltip">Help</button>
Follow the first rule of ARIA: “No ARIA is better than bad ARIA.” Only use ARIA when necessary, and prefer native HTML elements with built-in accessibility features whenever possible.

Accessible Images

Images need proper alternative text to be accessible to screen reader users.
<!-- Informative image with alt text -->
<img src="chart.png" alt="Bar chart showing sales growth of 25% in Q1 2023" />

<!-- Decorative image that should be ignored by screen readers -->
<img src="decorative-divider.png" alt="" />

<!-- Complex image with extended description -->
<figure>
  <img
    src="complex-diagram.png"
    alt="System architecture diagram"
    aria-describedby="diagram-desc"
  />
  <figcaption id="diagram-desc">
    The diagram shows the three-tier architecture with a presentation layer,
    business logic layer, and data access layer, connected by APIs.
  </figcaption>
</figure>

Keyboard Navigation

Ensure all interactive elements are keyboard accessible.
<!-- Custom button with keyboard support -->
<div
  role="button"
  tabindex="0"
  onclick="handleClick()"
  onkeydown="if(event.key === 'Enter' || event.key === ' ') handleClick()"
>
  Click me
</div>

<!-- Skip link for keyboard users -->
<a href="#main-content" class="skip-link">Skip to main content</a>

<!-- CSS for skip link -->
<style>
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    padding: 8px;
    background: #000;
    color: #fff;
    z-index: 100;
  }

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

CSS Accessibility Techniques

Focus Styles

Never remove focus outlines without providing an alternative.
/* Bad: Removing focus outline without alternative */
:focus {
  outline: none;
}

/* Good: Enhanced focus styles */
:focus {
  outline: 2px solid #4d90fe;
  outline-offset: 2px;
}

/* Better: Only modify for non-keyboard focus */
:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid #4d90fe;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(77, 144, 254, 0.3);
}

Color and Contrast

Ensure sufficient color contrast and don’t rely solely on color to convey information.
/* Bad: Low contrast text */
.low-contrast {
  color: #999;
  background-color: #777;
}

/* Good: High contrast text (4.5:1 ratio for normal text) */
.high-contrast {
  color: #222;
  background-color: #fff;
}

/* Don't rely solely on color for state indication */
.error {
  color: #d50000; /* Red for color users */
  border: 2px solid #d50000; /* Border for colorblind users */
}

.error::before {
  content: '⚠️ '; /* Symbol for additional indication */
}

Responsive Design

Ensure your site works at different zoom levels and viewport sizes.
/* Use relative units for better scaling */
body {
  font-size: 100%; /* Base font size */
}

h1 {
  font-size: 2em; /* Relative to base font size */
}

/* Ensure text can be resized up to 200% without breaking layout */
.container {
  max-width: 1200px;
  width: 90%;
  margin: 0 auto;
}

/* Support both portrait and landscape orientations */
@media screen and (orientation: portrait) {
  .gallery {
    flex-direction: column;
  }
}

@media screen and (orientation: landscape) {
  .gallery {
    flex-direction: row;
  }
}

Reduced Motion

Respect user preferences for reduced motion.
/* Default animation */
.animated {
  transition: transform 0.5s ease;
}

.animated:hover {
  transform: scale(1.1);
}

/* Respect prefers-reduced-motion setting */
@media (prefers-reduced-motion: reduce) {
  .animated {
    transition: none;
  }

  .animated:hover {
    transform: none;
  }
}

JavaScript Accessibility Techniques

Managing Focus

Manage keyboard focus when content changes dynamically.
// Focus management for modals
const openModal = () => {
  const modal = document.getElementById('modal');
  const closeButton = modal.querySelector('.close-button');

  // Show the modal
  modal.hidden = false;

  // Save the element that had focus before opening the modal
  modal.previousFocus = document.activeElement;

  // Set focus to the close button
  closeButton.focus();

  // Trap focus inside modal
  modal.addEventListener('keydown', trapFocus);
};

const closeModal = () => {
  const modal = document.getElementById('modal');

  // Hide the modal
  modal.hidden = true;

  // Restore focus to the element that had focus before the modal opened
  if (modal.previousFocus) {
    modal.previousFocus.focus();
  }

  // Remove focus trap
  modal.removeEventListener('keydown', trapFocus);
};

const trapFocus = (event) => {
  const modal = document.getElementById('modal');
  const focusableElements = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  // If Tab is pressed and Shift is not pressed and the active element is the last focusable element
  if (
    event.key === 'Tab' &&
    !event.shiftKey &&
    document.activeElement === lastElement
  ) {
    event.preventDefault();
    firstElement.focus();
  }

  // If Tab and Shift are pressed and the active element is the first focusable element
  if (
    event.key === 'Tab' &&
    event.shiftKey &&
    document.activeElement === firstElement
  ) {
    event.preventDefault();
    lastElement.focus();
  }

  // Close on Escape
  if (event.key === 'Escape') {
    closeModal();
  }
};

Accessible Custom Components

Implement accessibility patterns for custom UI components.
// Accessible dropdown menu
class Dropdown {
  constructor(element) {
    this.dropdown = element;
    this.button = element.querySelector('[aria-haspopup="true"]');
    this.menu = element.querySelector('[role="menu"]');
    this.menuItems = element.querySelectorAll('[role="menuitem"]');
    this.activeIndex = 0;

    this.button.addEventListener('click', () => this.toggleMenu());
    this.button.addEventListener('keydown', (e) => this.handleButtonKeyDown(e));
    this.menu.addEventListener('keydown', (e) => this.handleMenuKeyDown(e));

    // Close when clicking outside
    document.addEventListener('click', (e) => {
      if (!this.dropdown.contains(e.target)) {
        this.closeMenu();
      }
    });
  }

  toggleMenu() {
    const isExpanded = this.button.getAttribute('aria-expanded') === 'true';

    if (isExpanded) {
      this.closeMenu();
    } else {
      this.openMenu();
    }
  }

  openMenu() {
    this.button.setAttribute('aria-expanded', 'true');
    this.menu.hidden = false;

    // Set focus on first menu item
    this.activeIndex = 0;
    this.menuItems[this.activeIndex].focus();
  }

  closeMenu() {
    this.button.setAttribute('aria-expanded', 'false');
    this.menu.hidden = true;
    this.button.focus();
  }

  handleButtonKeyDown(event) {
    switch (event.key) {
      case 'ArrowDown':
      case 'Down': // For IE/Edge
        event.preventDefault();
        this.openMenu();
        break;
      case 'Escape':
        this.closeMenu();
        break;
    }
  }

  handleMenuKeyDown(event) {
    switch (event.key) {
      case 'ArrowDown':
      case 'Down': // For IE/Edge
        event.preventDefault();
        this.activeIndex = (this.activeIndex + 1) % this.menuItems.length;
        this.menuItems[this.activeIndex].focus();
        break;
      case 'ArrowUp':
      case 'Up': // For IE/Edge
        event.preventDefault();
        this.activeIndex =
          (this.activeIndex - 1 + this.menuItems.length) %
          this.menuItems.length;
        this.menuItems[this.activeIndex].focus();
        break;
      case 'Home':
        event.preventDefault();
        this.activeIndex = 0;
        this.menuItems[this.activeIndex].focus();
        break;
      case 'End':
        event.preventDefault();
        this.activeIndex = this.menuItems.length - 1;
        this.menuItems[this.activeIndex].focus();
        break;
      case 'Escape':
        this.closeMenu();
        break;
      case 'Enter':
      case ' ':
        event.preventDefault();
        this.menuItems[this.activeIndex].click();
        break;
    }
  }
}

// Initialize all dropdowns
document.querySelectorAll('.dropdown').forEach((dropdown) => {
  new Dropdown(dropdown);
});

Accessible Notifications

Implement accessible notifications for dynamic content changes.
// Function to create an accessible notification
function notify(message, type = 'polite') {
  // Create or get the live region
  let liveRegion = document.getElementById('notifications');

  if (!liveRegion) {
    liveRegion = document.createElement('div');
    liveRegion.id = 'notifications';
    liveRegion.setAttribute('aria-live', type); // 'polite' or 'assertive'
    liveRegion.setAttribute('aria-atomic', 'true');
    liveRegion.style.position = 'absolute';
    liveRegion.style.width = '1px';
    liveRegion.style.height = '1px';
    liveRegion.style.overflow = 'hidden';
    liveRegion.style.clip = 'rect(0, 0, 0, 0)';
    document.body.appendChild(liveRegion);
  }

  // Clear previous messages
  liveRegion.textContent = '';

  // Set a small timeout to ensure the screen reader notices the change
  setTimeout(() => {
    liveRegion.textContent = message;
  }, 50);
}

// Example usage
document.querySelector('form').addEventListener('submit', (event) => {
  event.preventDefault();
  // Process form submission
  notify('Your form has been submitted successfully', 'assertive');
});

Testing for Accessibility

Automated Testing

Automated tools can catch many common accessibility issues.
2

Integrate accessibility testing in your build process

# Using axe-core with Jest
npm install --save-dev jest-axe
// Example Jest test with axe
import { axe } from 'jest-axe';

test('Component should not have accessibility violations', async () => {
  const html = document.body.innerHTML;
  const results = await axe(html);
  expect(results).toHaveNoViolations();
});
3

Use linting tools

# Install eslint-plugin-jsx-a11y for React projects
npm install --save-dev eslint-plugin-jsx-a11y
// .eslintrc
{
  "plugins": ["jsx-a11y"],
  "extends": ["plugin:jsx-a11y/recommended"]
}

Manual Testing

Automated tests can’t catch everything. Manual testing is essential.

User Testing

The most valuable accessibility testing involves real users with disabilities.
  • Partner with accessibility consultants or organizations
  • Recruit users with different disabilities for usability testing
  • Consider different assistive technologies and usage patterns
  • Document and prioritize issues discovered during testing

React

// Accessible React component example
import React, { useState, useRef, useEffect } from 'react';

function Modal({ isOpen, onClose, title, children }) {
  const [isVisible, setIsVisible] = useState(isOpen);
  const closeButtonRef = useRef(null);
  const modalRef = useRef(null);
  const previousFocusRef = useRef(null);

  // Sync with props
  useEffect(() => {
    setIsVisible(isOpen);
  }, [isOpen]);

  // Focus management
  useEffect(() => {
    if (isVisible) {
      // Store the current active element
      previousFocusRef.current = document.activeElement;
      // Focus the close button when modal opens
      closeButtonRef.current.focus();
    } else if (previousFocusRef.current) {
      // Restore focus when modal closes
      previousFocusRef.current.focus();
    }
  }, [isVisible]);

  // Close on escape key
  useEffect(() => {
    const handleKeyDown = (event) => {
      if (event.key === 'Escape' && isVisible) {
        onClose();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isVisible, onClose]);

  if (!isVisible) return null;

  return (
    <div
      className="modal-overlay"
      onClick={onClose}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <div
        className="modal-content"
        ref={modalRef}
        onClick={(e) => e.stopPropagation()}
      >
        <div className="modal-header">
          <h2 id="modal-title">{title}</h2>
          <button
            ref={closeButtonRef}
            className="close-button"
            onClick={onClose}
            aria-label="Close modal"
          >
            ×
          </button>
        </div>
        <div className="modal-body">{children}</div>
      </div>
    </div>
  );
}

export default Modal;

Vue.js

<!-- Accessible Vue component example -->
<template>
  <div>
    <button @click="isOpen = true" aria-haspopup="dialog">Open Modal</button>

    <div
      v-if="isOpen"
      class="modal-overlay"
      @click="closeModal"
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
    >
      <div class="modal-content" @click.stop ref="modalContent">
        <div class="modal-header">
          <h2 id="modal-title">{{ title }}</h2>
          <button
            ref="closeButton"
            class="close-button"
            @click="closeModal"
            aria-label="Close modal"
          >
            ×
          </button>
        </div>
        <div class="modal-body">
          <slot></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      isOpen: false,
      previousFocus: null,
    };
  },
  watch: {
    isOpen(newValue) {
      if (newValue) {
        this.$nextTick(() => {
          // Store previous focus
          this.previousFocus = document.activeElement;
          // Focus the close button
          this.$refs.closeButton.focus();
          // Add event listeners
          document.addEventListener('keydown', this.handleKeyDown);
        });
      } else {
        // Remove event listeners
        document.removeEventListener('keydown', this.handleKeyDown);
        // Restore focus
        if (this.previousFocus) {
          this.previousFocus.focus();
        }
      }
    },
  },
  methods: {
    closeModal() {
      this.isOpen = false;
    },
    handleKeyDown(event) {
      if (event.key === 'Escape') {
        this.closeModal();
      }
    },
  },
  beforeDestroy() {
    document.removeEventListener('keydown', this.handleKeyDown);
  },
};
</script>

Angular

// modal.component.ts
import {
  Component,
  Input,
  Output,
  EventEmitter,
  ElementRef,
  OnInit,
  OnDestroy,
  ViewChild,
} from '@angular/core';

@Component({
  selector: 'app-modal',
  template: `
    <div
      *ngIf="isOpen"
      class="modal-overlay"
      (click)="close()"
      role="dialog"
      aria-modal="true"
      [attr.aria-labelledby]="'modal-title-' + id"
    >
      <div class="modal-content" (click)="$event.stopPropagation()">
        <div class="modal-header">
          <h2 [id]="'modal-title-' + id">{{ title }}</h2>
          <button
            #closeButton
            class="close-button"
            (click)="close()"
            aria-label="Close modal"
          >
            ×
          </button>
        </div>
        <div class="modal-body">
          <ng-content></ng-content>
        </div>
      </div>
    </div>
  `,
})
export class ModalComponent implements OnInit, OnDestroy {
  @Input() isOpen = false;
  @Input() title = '';
  @Input() id = 'modal';
  @Output() closed = new EventEmitter<void>();
  @ViewChild('closeButton') closeButton: ElementRef;

  private previousFocus: HTMLElement | null = null;
  private keydownListener: (event: KeyboardEvent) => void;

  constructor(private elementRef: ElementRef) {
    this.keydownListener = this.handleKeyDown.bind(this);
  }

  ngOnInit() {
    document.addEventListener('keydown', this.keydownListener);
  }

  ngOnChanges(changes) {
    if (changes.isOpen) {
      if (changes.isOpen.currentValue) {
        // Modal opened
        this.previousFocus = document.activeElement as HTMLElement;
        setTimeout(() => {
          this.closeButton.nativeElement.focus();
        });
      } else if (this.previousFocus) {
        // Modal closed
        this.previousFocus.focus();
      }
    }
  }

  close() {
    this.closed.emit();
  }

  handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Escape' && this.isOpen) {
      this.close();
    }
  }

  ngOnDestroy() {
    document.removeEventListener('keydown', this.keydownListener);
  }
}

Accessibility Checklist

Use this checklist to ensure your website meets basic accessibility requirements:
1

Document Structure

  • Use semantic HTML elements (<header>, <nav>, <main>, <section>, etc.)
  • Ensure proper heading hierarchy (<h1> through <h6>)
  • Include a page <title> that describes the page content
  • Provide a skip link to bypass navigation
  • Use landmarks appropriately (header, nav, main, footer)
2

Text and Typography

  • Ensure sufficient color contrast (4.5:1 for normal text, 3:1 for large text) - [ ] Don’t use color alone to convey information - [ ] Use relative units for font sizes (em, rem) - [ ] Ensure text can be resized up to 200% without loss of content - [ ] Maintain line spacing of at least 1.5 within paragraphs
3

Images and Media

  • Provide alternative text for all informative images - [ ] Use empty alt text for decorative images - [ ] Include captions and transcripts for audio/video content - [ ] Ensure media doesn’t autoplay - [ ] Provide controls for media playback
4

Keyboard and Focus

  • Ensure all functionality is keyboard accessible - [ ] Maintain a logical tab order - [ ] Provide visible focus indicators - [ ] Ensure no keyboard traps - [ ] Manage focus for interactive components
5

Forms and Inputs

  • Associate labels with form controls - [ ] Group related form elements with fieldset and legend - [ ] Provide clear error messages and validation - [ ] Use appropriate input types (email, tel, etc.) - [ ] Ensure form controls have accessible names
6

Dynamic Content

  • Use ARIA attributes appropriately - [ ] Announce dynamic content changes
  • Ensure custom widgets follow WAI-ARIA patterns - [ ] Provide status messages for operations - [ ] Allow sufficient time for users to read content
7

Testing

  • Test with keyboard navigation
  • Test with screen readers
  • Test at different zoom levels
  • Validate HTML
  • Run automated accessibility tests

Resources

Tools and Testing

Guidelines and Documentation

Learning Resources

Next Steps

Now that you understand web accessibility, you can: