Back to Blog
|8 min read

Building Accessible React Components from Scratch

A deep dive into creating truly accessible UI components using React, covering ARIA patterns, keyboard navigation, and focus management.

ReactAccessibilityTypeScript

Why Accessibility Matters

Building accessible web applications isn't just about compliance—it's about creating experiences that work for everyone. When we build with accessibility in mind, we create better products for all users, not just those with disabilities.

In this article, I'll walk you through the patterns I use daily to build truly accessible React components.

The Foundation: Semantic HTML

Before reaching for ARIA attributes, start with semantic HTML. It's the foundation of accessible web development:

// Instead of this:
<div onClick={handleClick}>Click me</div>

// Do this:
<button onClick={handleClick}>Click me</button>

Semantic elements provide built-in accessibility features:

  • Keyboard navigation comes for free
  • Screen readers understand the element's purpose
  • Focus management works automatically

ARIA Patterns for Custom Components

Sometimes semantic HTML isn't enough. When building custom components like dropdowns or modals, ARIA attributes become essential.

Example: Accessible Dropdown Menu

function Dropdown({ items, label }: DropdownProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  
  const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(prev => Math.min(prev + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
    <div onKeyDown={handleKeyDown}>
      <button aria-haspopup="listbox" aria-expanded={isOpen} onClick={() => setIsOpen(!isOpen)}>
        {label}
      </button>
      {isOpen && (
        <ul role="listbox" aria-label={label}>
          {items.map((item, index) => (
            <li key={item.id} role="option" aria-selected={index === activeIndex}>
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Focus Management

One of the trickiest aspects of accessibility is managing focus. Here are some key principles:

  1. Trap focus in modals - Users shouldn't be able to tab outside a modal
  2. Return focus on close - When closing a modal, return focus to the trigger
  3. Skip links - Provide a way to skip repetitive navigation

Testing Accessibility

Building accessible components is only half the battle—you need to test them too:

ToolPurpose
axe-coreAutomated accessibility testing
jest-axeJest integration for a11y tests
VoiceOver/NVDAManual screen reader testing
Keyboard testingNavigate without a mouse

Key Takeaways

  • Start with semantic HTML before reaching for ARIA
  • Implement keyboard navigation for all interactive elements
  • Manage focus carefully in modals and dynamic content
  • Test with real assistive technologies, not just automated tools
  • Remember that accessibility benefits everyone