Skip to content

Accessibility

Building an inclusive web experience for everyone.

WCAG Compliance

This site meets WCAG 2.1 AA standards across all pages.

Conformance Levels

  • ✅ Level A: All criteria met
  • ✅ Level AA: All criteria met
  • ⚠️ Level AAA: Partial (where applicable)

Key Features

1. Semantic HTML

Proper HTML structure for assistive technologies:

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

<main>
  <article>
    <h1>Page Title</h1>
    <p>Content...</p>
  </article>
</main>

<footer>
  <!-- Footer content -->
</footer>

2. Keyboard Navigation

Full site navigation via keyboard:

  • Tab: Move forward through interactive elements
  • Shift + Tab: Move backward
  • Enter/Space: Activate buttons and links
  • Escape: Close modals and menus

Focus Indicators:

:focus-visible {
  outline: 2px solid oklch(0.7 0.2 250);
  outline-offset: 2px;
}

3. Screen Reader Support

ARIA Labels:

<button aria-label="Toggle dark mode" aria-pressed={isDark}>
  <span aria-hidden="true">🌙</span>
</button>

Live Regions:

<div role="status" aria-live="polite" aria-atomic="true">
  {message}
</div>

Skip Links:

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

4. Color and Contrast

Contrast Ratios:

  • Normal text: 7:1 (AAA)
  • Large text: 4.5:1 (AA)
  • Interactive elements: 3:1 (AA)

Color Independence:

Information never conveyed by color alone:

<!-- Bad -->
<span style="color: red;">Error</span>

<!-- Good -->
<span class="error">
  <span aria-label="Error">⚠️</span>
  Error message
</span>

5. Motion and Animation

Respects reduced motion preferences:

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

6. Form Accessibility

Proper labels and error handling:

<label for="email">
  Email Address
  <span aria-label="required">*</span>
</label>
<input
  type="email"
  id="email"
  name="email"
  aria-required="true"
  aria-invalid={hasError}
  aria-describedby="email-error"
/>
<span id="email-error" role="alert">
  {errorMessage}
</span>

Testing

Automated Testing

axe-core Integration:

import { axe } from 'axe-core';

describe('Accessibility', () => {
  it('has no violations', async () => {
    const results = await axe(document.body);
    expect(results.violations).toHaveLength(0);
  });
});

Manual Testing

Screen Readers:

  • ✅ NVDA (Windows)
  • ✅ JAWS (Windows)
  • ✅ VoiceOver (macOS/iOS)
  • ✅ TalkBack (Android)

Keyboard Only:

  • ✅ All interactive elements reachable
  • ✅ Logical tab order
  • ✅ Visible focus indicators

Browser Testing

Tested across:

  • Chrome/Edge with ChromeVox
  • Firefox with NVDA
  • Safari with VoiceOver

Common Patterns

Interactive Buttons

interface ButtonProps {
  onClick: () => void;
  ariaLabel?: string;
  disabled?: boolean;
}

export default function Button({ onClick, ariaLabel, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} aria-label={ariaLabel} disabled={disabled} aria-disabled={disabled}>
      Click me
    </button>
  );
}

Expandable Sections

<button aria-expanded={isOpen} aria-controls="section-content" onclick="toggleSection()">
  Toggle Section
</button>

<div id="section-content" hidden={!isOpen}>Content here</div>
export default function Modal({ isOpen, onClose }) {
  useEffect(() => {
    if (isOpen) {
      // Trap focus
      // Set focus to first element
    }
  }, [isOpen]);

  return (
    <div role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden={!isOpen}>
      <h2 id="modal-title">Modal Title</h2>
      <button onClick={onClose}>Close</button>
    </div>
  );
}

Best Practices

Images

Decorative images:

<img src="decoration.png" alt="" role="presentation" />

Informative images:

<img src="chart.png" alt="Sales increased 40% in Q4" />

Descriptive link text:

<!-- Bad -->
<a href="/docs">Click here</a>

<!-- Good -->
<a href="/docs">Read the documentation</a>

Headings

Logical hierarchy:

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

Resources

Next Steps