Accessibility is not a feature for a small percentage of users. It is a fundamental quality of web experiences. At least 15% of people have disabilities affecting how they interact with technology. And accessibility improvements benefit everyone: captions help in noisy environments, keyboard navigation helps power users, clear structure helps mobile users, good contrast helps in bright sunlight.

Building accessible applications requires understanding how assistive technologies work, following established guidelines, and testing with the tools disabled users rely on. It is not just about screen readers; it is about color contrast, keyboard navigation, cognitive load, and flexible interfaces that adapt to user needs.

I have built accessible interfaces, conducted accessibility audits, and worked with users of assistive technologies. I have learned that semantic HTML eliminates many accessibility problems, that ARIA should be a last resort, and that automated testing catches only a fraction of issues. This guide covers the patterns that work: understanding WCAG guidelines and compliance levels, semantic HTML as the foundation, ARIA patterns for complex components, keyboard navigation implementation, testing with assistive technologies, and building inclusive design systems.

Understanding Disability and Accessibility

Types of Disabilities

Visual: Blindness, low vision, color blindness Auditory: Deafness, hearing impairment Motor: Limited dexterity, inability to use mouse Cognitive: Learning disabilities, attention deficits, memory issues

Assistive Technologies

Screen readers: NVDA, JAWS, VoiceOver Screen magnifiers: ZoomText Switch controls: Single-button navigation Voice control: Dragon NaturallySpeaking

WCAG 2.1 Guidelines

The Four Principles (POUR)

Perceivable: Information must be presentable in ways users can perceive Operable: Interface components must be operable by all users Understandable: Information and UI operation must be understandable Robust: Content must work with current and future assistive technologies

Compliance Levels

Level A: Minimum accessibility (must have) Level AA: Strong accessibility (should have) - Legal requirement in many jurisdictions. For a step-by-step auditing process, read our website ADA and WCAG compliance guide. Level AAA: Excellent accessibility (ideal to have)

Key Requirements

Perceivable:

  • Text alternatives for images
  • Captions/transcripts for multimedia
  • Minimum 4.5:1 contrast ratio (AA)
  • Resizable text up to 200%

Operable:

  • All functionality available by keyboard
  • No keyboard traps
  • Sufficient time to complete tasks
  • No seizure-inducing content

Understandable:

  • Readable text (simplified language for AAA)
  • Consistent navigation
  • Error prevention and recovery
  • Input assistance

Robust:

  • Valid HTML
  • Compatible with assistive technologies
  • Status messages announced

Semantic HTML

The Foundation

Semantic HTML provides accessibility for free. Use it before adding ARIA.

<!-- Bad: Generic divs -->
<div class="header">...</div>
<div class="nav">...</div>
<div class="main">
  <div class="article">
    <div class="h1">Title</div>
  </div>
</div>

<!-- Good: Semantic elements -->
<header>...</header>
<nav>...</nav>
<main>
  <article>
    <h1>Title</h1>
  </article>
</main>

Headings

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

Forms

<form>
  <!-- Associated label -->
  <label for="email">Email Address</label>
  <input 
    type="email" 
    id="email" 
    name="email"
    required
    aria-required="true"
    aria-describedby="email-error"
  >
  <span id="email-error" role="alert" class="error"></span>
  
  <!-- Fieldset for groups -->
  <fieldset>
    <legend>Preferred Contact Method</legend>
    
    <input type="radio" id="contact-email" name="contact" value="email">
    <label for="contact-email">Email</label>
    
    <input type="radio" id="contact-phone" name="contact" value="phone">
    <label for="contact-phone">Phone</label>
  </fieldset>
</form>

Landmarks

<header role="banner">
  <nav role="navigation" aria-label="Main">
    <!-- Navigation links -->
  </nav>
</header>

<main role="main">
  <article>
    <h1>Article Title</h1>
  </article>
</main>

<aside role="complementary" aria-label="Related articles">
  <!-- Related content -->
</aside>

<footer role="contentinfo">
  <!-- Copyright, links -->
</footer>

ARIA Patterns

When to Use ARIA

Use when:

  • Creating custom components (tabs, modals, accordions)
  • Providing additional context
  • Dynamic content updates

Do not use when:

  • Native HTML element exists
  • Semantic HTML suffices
  • Adding ARIA to hide semantics

Common Patterns

Button:

<!-- Custom button -->
<div role="button" 
     tabindex="0"
     aria-pressed="false"
     onclick="handleClick()"
     onkeydown="handleKeydown(event)">
  Click me
</div>

Modal Dialog:

<div role="dialog" 
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-description">
  <h2 id="dialog-title">Confirm Delete</h2>
  <p id="dialog-description">
    This will permanently delete the item.
  </p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

Tabs:

<div class="tabs">
  <div role="tablist" aria-label="Product Sections">
    <button role="tab" 
            aria-selected="true" 
            aria-controls="panel-1"
            id="tab-1">
      Description
    </button>
    <button role="tab" 
            aria-selected="false"
            aria-controls="panel-2"
            id="tab-2"
            tabindex="-1">
      Specifications
    </button>
  </div>
  
  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
    <!-- Description content -->
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
    <!-- Specifications content -->
  </div>
</div>

Live Regions:

<!-- Status announcements -->
<div id="status" role="status" aria-live="polite" aria-atomic="true">
  <!-- Content changes announced to screen readers -->
</div>

<!-- Alert announcements -->
<div id="alert" role="alert" aria-live="assertive">
  <!-- Urgent messages -->
</div>

Keyboard Navigation

Focus Management

// Visible focus indicator
:focus {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}

// Focus trap for modals
class Modal {
  open() {
    this.previousFocus = document.activeElement;
    this.modal.showModal();
    
    // Trap focus
    this.modal.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        this.trapFocus(e);
      }
    });
    
    // Focus first element
    this.focusableElements[0]?.focus();
  }
  
  close() {
    this.modal.close();
    // Restore focus
    this.previousFocus?.focus();
  }
  
  trapFocus(event) {
    const focusable = this.focusableElements;
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
}

Keyboard Shortcuts

// Accessible keyboard handling
document.addEventListener('keydown', (e) => {
  // Skip link
  if (e.key === 'Tab' && e.target === document.body) {
    document.getElementById('skip-link').focus();
  }
  
  // Escape to close modals
  if (e.key === 'Escape') {
    closeOpenModals();
  }
  
  // Arrow keys for custom components
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
    if (isInListbox(e.target)) {
      navigateListbox(e);
    }
  }
});

Testing Accessibility

Automated Testing

Automated tools are an excellent starting point for scanning your site for common accessibility violations. For websites running on WordPress, you can leverage dedicated plugins to automate and monitor these checks; see our guide to the best WordPress plugins for ADA and WCAG compliance to choose the right solution.

For developers and custom builds, CLI and testing libraries offer programmatic validation:

# axe-core CLI
axe https://example.com --tags wcag2aa

# Pa11y
pa11y https://example.com --standard WCAG2AA

# Lighthouse
lighthouse https://example.com --only-categories=accessibility

# Jest + jest-axe
npm install --save-dev jest-axe
// jest-axe example
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import Button from './Button';

expect.extend(toHaveNoViolations);

test('Button has no accessibility violations', async () => {
  const { container } = render(<Button>Click me</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Manual Testing

Keyboard-only navigation:

  1. Unplug mouse
  2. Navigate entire page with Tab, Shift+Tab, Enter, Space, Arrow keys
  3. Verify all functionality works
  4. Check focus visibility

Screen reader testing:

NVDA (Windows, free):
- Install NVDA
- Navigate with Insert + Arrow keys
- Read element: Insert + Tab

VoiceOver (macOS, built-in):
- Enable: Cmd + F5
- Navigate: Cmd + Option + Arrow keys
- Read element: Ctrl + Option + Cmd + T

Color and Contrast

# WAVE browser extension
# Check contrast errors

# axe DevTools
# Shows contrast ratios

Manual check:

  1. Screenshot in grayscale
  2. Verify all information is still conveyed
  3. Check that color is not the only indicator

Inclusive Design

Cognitive Accessibility

/* Readable text */
body {
  font-family: system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.5;
  max-width: 70ch;
}

/* Clear focus */
:focus {
  outline: 3px solid;
  outline-offset: 3px;
}

/* Reduce motion */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Responsive and Flexible

/* Text resizing support */
html {
  font-size: 100%; /* Respect user preference */
}

/* Container queries for flexibility */
.card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card-content {
    display: grid;
    grid-template-columns: 1fr 2fr;
  }
}

/* Touch target sizing */
button, .button {
  min-height: 44px;
  min-width: 44px;
  padding: 12px 16px;
}

Testing Checklist

## Accessibility Checklist

### Visual
- [ ] Color contrast 4.5:1 for text (AA)
- [ ] Information not conveyed by color alone
- [ ] Text resizes to 200% without loss
- [ ] Focus indicators visible

### Motor
- [ ] All functionality keyboard accessible
- [ ] No keyboard traps
- [ ] Skip links provided
- [ ] Sufficient time for interactions

### Hearing
- [ ] Captions for video
- [ ] Transcripts for audio
- [ ] Visual alternatives to audio cues

### Cognitive
- [ ] Clear, simple language
- [ ] Consistent navigation
- [ ] Error prevention and recovery
- [ ] Reduced motion support

Accessibility Culture

Team Practices

Shift-left testing:

  • Accessibility in design reviews
  • Component-level accessibility testing
  • Automated checks in CI/CD

Documentation:

  • A11y acceptance criteria in tickets
  • Pattern library with accessibility notes
  • Decision log for accessibility trade-offs

Regulations:

  • ADA (US)
  • Section 508 (US Government)
  • EN 301 549 (EU)
  • AODA (Canada)

Business benefits:

  • Larger addressable market
  • Better SEO (semantic HTML)
  • Reduced legal risk
  • Improved usability for all

Conclusion

Accessibility is quality. Build it in from the start using semantic HTML. Use ARIA sparingly for custom components. Test with keyboards and screen readers. Maintain sufficient contrast and flexible layouts.

The web was designed to work for everyone. Our implementations should honor that principle. Accessible design benefits all users while being essential for some.


Further Reading