Building Truly Accessible React Components

2024-03-25

Building accessible web applications isn't just about compliance—it's about creating inclusive experiences that work for everyone. In this guide, we'll explore how to build React components that are truly accessible, backed by real-world examples and research.

The Business Case for Accessibility

Recent studies make a compelling case for accessibility:

Real-World Success Stories

Case Study 1: BBC iPlayer

The BBC's iPlayer service implemented comprehensive keyboard navigation and screen reader support. Results:

Let's look at their keyboard navigation pattern:

const MediaPlayer = () => {
  const [isPlaying, setIsPlaying] = useState(false);
  const videoRef = useRef<HTMLVideoElement>(null);

  const handleKeyPress = (event: KeyboardEvent) => {
    switch(event.key) {
      case ' ':
      case 'k': // YouTube-style shortcut
        event.preventDefault();
        setIsPlaying(!isPlaying);
        break;
      case 'j':
        // Rewind 10 seconds
        videoRef.current!.currentTime -= 10;
        break;
      case 'l':
        // Forward 10 seconds
        videoRef.current!.currentTime += 10;
        break;
      case 'm':
        // Toggle mute
        videoRef.current!.muted = !videoRef.current!.muted;
        break;
    }
  };

  return (
    <div 
      role="region"
      aria-label="Video Player"
      onKeyDown={handleKeyPress}
      tabIndex={0}
    >
      <video ref={videoRef}>
        {/* Video content */}
      </video>
      <div className="controls" aria-label="Video Controls">
        {/* Custom accessible controls */}
      </div>
    </div>
  );
};

Case Study 2: Gov.uk Forms

The UK Government Digital Service created an accessible form system that reduced error rates by 93%. Their pattern:

const AccessibleForm = () => {
  const [errors, setErrors] = useState<Record<string, string>>({});
  const formRef = useRef<HTMLFormElement>(null);

  const validate = (data: FormData) => {
    const newErrors: Record<string, string> = {};
    
    // Validation logic
    if (!data.get('email')) {
      newErrors.email = 'Email is required';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (event: FormEvent) => {
    event.preventDefault();
    const data = new FormData(event.target as HTMLFormElement);
    
    if (validate(data)) {
      // Submit form
    } else {
      // Focus first error
      const firstErrorId = Object.keys(errors)[0];
      document.getElementById(firstErrorId)?.focus();
      
      // Announce errors to screen readers
      announceErrors(errors);
    }
  };

  return (
    <form
      ref={formRef}
      onSubmit={handleSubmit}
      noValidate
    >
      <div className="form-group">
        <label htmlFor="email">
          Email address
          {errors.email && (
            <span className="error" role="alert">
              {errors.email}
            </span>
          )}
        </label>
        <input
          id="email"
          type="email"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
        />
      </div>
      {/* More form fields */}
    </form>
  );
};

Case Study 3: Airbnb's Calendar Component

Airbnb's date picker implementation shows how to handle complex interactions accessibly:

const Calendar = () => {
  const [selectedDate, setSelectedDate] = useState<Date>();
  const [focusedDate, setFocusedDate] = useState<Date>();
  
  const announceSelectedDate = (date: Date) => {
    const announcement = `Selected date: ${date.toLocaleDateString()}`;
    // Use a live region to announce changes
    document.getElementById('date-announcer')?.textContent = announcement;
  };

  const handleDateSelect = (date: Date) => {
    setSelectedDate(date);
    announceSelectedDate(date);
  };

  const handleKeyboardNavigation = (event: KeyboardEvent) => {
    let newDate = focusedDate;
    
    switch(event.key) {
      case 'ArrowRight':
        newDate = addDays(focusedDate!, 1);
        break;
      case 'ArrowLeft':
        newDate = addDays(focusedDate!, -1);
        break;
      case 'ArrowUp':
        newDate = addDays(focusedDate!, -7);
        break;
      case 'ArrowDown':
        newDate = addDays(focusedDate!, 7);
        break;
    }

    if (newDate) {
      event.preventDefault();
      setFocusedDate(newDate);
    }
  };

  return (
    <>
      <div
        role="application"
        aria-label="Calendar"
        onKeyDown={handleKeyboardNavigation}
      >
        {/* Calendar grid */}
      </div>
      <div
        id="date-announcer"
        role="status"
        aria-live="polite"
        className="sr-only"
      />
    </>
  );
};

Testing with Real Users

Microsoft's Inclusive Design team found that testing with users with disabilities led to:

Here's a testing checklist based on their findings:

describe('Component Accessibility', () => {
  it('supports keyboard navigation', async () => {
    const { container } = render(<YourComponent />);
    
    // Tab navigation
    userEvent.tab();
    expect(document.activeElement).toHaveAttribute('aria-label');
    
    // Arrow key navigation
    userEvent.keyboard('{arrowdown}');
    expect(document.activeElement).toHaveAttribute('aria-selected');
  });

  it('announces status changes', async () => {
    const { getByRole } = render(<YourComponent />);
    
    // Find live region
    const status = getByRole('status');
    
    // Trigger state change
    userEvent.click(getByRole('button'));
    
    // Verify announcement
    expect(status).toHaveTextContent('Status updated');
  });
});

Tools and Resources

  1. Automated Testing
  1. Screen Readers
  1. Development Tools

Conclusion

Building accessible components is not just about following guidelines—it's about understanding real user needs and implementing solutions that work for everyone. The examples and studies above show that accessibility is both technically achievable and business-critical.

Remember to:

Your investment in accessibility will pay off in better usability, broader reach, and reduced legal risk.

Further Reading