Accessibility Standards

Accessibility is not optional—it's a fundamental requirement for every component in your design system. Accessible components ensure that all users, regardless of ability, can effectively use your products. These standards provide the foundation for creating inclusive, compliant components.

Why Accessibility Matters

Accessible components:

  • Legal Compliance: Meet WCAG 2.1 Level AA requirements
  • User Inclusion: Serve users with disabilities
  • Better UX: Improve experience for all users
  • Business Value: Expand your user base

Core Requirements

1. Semantic HTML

Use semantic HTML elements that convey meaning:

// ❌ Bad: Generic elements
<div onClick={handleClick}>Click me</div>
<span role="button">Submit</span>

// ✅ Good: Semantic elements
<button onClick={handleClick}>Click me</button>
<button type="submit">Submit</button>

2. Keyboard Navigation

All interactive elements must be keyboard accessible:

// ✅ Good: Keyboard accessible
<button 
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick();
    }
  }}
>
  Click me
</button>

// Native button already handles keyboard - no extra code needed!
<button onClick={handleClick}>Click me</button>

3. ARIA Attributes

Use ARIA when HTML semantics aren't sufficient:

// ✅ Good: ARIA for complex components
<div 
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-description"
>
  <h2 id="dialog-title">Confirm Action</h2>
  <p id="dialog-description">Are you sure you want to continue?</p>
</div>

// ✅ Good: ARIA for dynamic content
<div aria-live="polite" aria-atomic="true">
  {loading ? 'Loading...' : 'Content loaded'}
</div>

4. Color Contrast

Ensure sufficient color contrast for text:

  • Normal text: 4.5:1 contrast ratio (WCAG AA)
  • Large text: 3:1 contrast ratio (WCAG AA)
  • UI components: 3:1 contrast ratio

5. Focus Management

Ensure focus is visible and properly managed:

// ✅ Good: Visible focus indicator
.button:focus-visible {
  outline: 2px solid var(--semantic-color-border-focus);
  outline-offset: 2px;
}

// ✅ Good: Focus trapping in modals
function Modal({ isOpen, onClose, children }) {
  useEffect(() => {
    if (isOpen) {
      // Trap focus inside modal
      const focusableElements = modalRef.current.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstElement = focusableElements[0];
      const lastElement = focusableElements[focusableElements.length - 1];
      
      const handleTab = (e) => {
        if (e.key === 'Tab') {
          if (e.shiftKey && document.activeElement === firstElement) {
            e.preventDefault();
            lastElement.focus();
          } else if (!e.shiftKey && document.activeElement === lastElement) {
            e.preventDefault();
            firstElement.focus();
          }
        }
      };
      
      firstElement?.focus();
      document.addEventListener('keydown', handleTab);
      return () => document.removeEventListener('keydown', handleTab);
    }
  }, [isOpen]);
  
  return <div ref={modalRef}>{children}</div>;
}

Component-Specific Guidelines

Buttons

  • Use semantic <button> element
  • Provide accessible label (text or aria-label)
  • Indicate loading state with aria-busy
  • Minimum 44x44px touch target

Form Controls

  • Associate labels with inputs using htmlFor/id
  • Provide error messages with aria-describedby
  • Use aria-required for required fields
  • Announce validation errors to screen readers

Dialogs/Modals

  • Use role="dialog" or <dialog> element
  • Provide aria-labelledby for title
  • Trap focus inside modal
  • Return focus to trigger when closed

Testing Requirements

Automated Testing

Run automated accessibility tests:

// jest-axe for unit tests
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

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

Manual Testing

Test with assistive technologies:

  • Screen readers: VoiceOver (macOS/iOS), NVDA/JAWS (Windows)
  • Keyboard only: Navigate without mouse
  • Zoom: Test at 200% zoom
  • High contrast: Test in high contrast mode

Common Pitfalls

1. Missing Labels

// ❌ Bad: No label
<input type="text" />

// ✅ Good: Associated label
<label htmlFor="email">Email</label>
<input id="email" type="email" />

2. Keyboard Traps

// ❌ Bad: Focus can't escape
<div onKeyDown={(e) => e.preventDefault()}>
  {/* Focus trapped */}
</div>

// ✅ Good: Proper focus management
<div 
  onKeyDown={(e) => {
    if (e.key === 'Escape') {
      onClose();
    }
  }}
>
  {/* Focus can escape */}
</div>

3. Insufficient Color Contrast

// ❌ Bad: Low contrast
.button {
  background: #ccc;
  color: #ddd; /* Contrast ratio: 1.2:1 */
}

// ✅ Good: Sufficient contrast
.button {
  background: #0066cc;
  color: #ffffff; /* Contrast ratio: 4.5:1 */
}

4. Missing ARIA Labels

// ❌ Bad: No indication of purpose
<button onClick={handleClose}>
  <Icon name="close" />
</button>

// ✅ Good: Clear label
<button 
  onClick={handleClose}
  aria-label="Close dialog"
>
  <Icon name="close" />
</button>

WCAG Compliance

Level AA Requirements

All components must meet WCAG 2.1 Level AA:

  • Perceivable: Text alternatives, captions, color contrast
  • Operable: Keyboard accessible, no seizures, enough time
  • Understandable: Readable, predictable, input assistance
  • Robust: Compatible with assistive technologies

Related Resources