Skip to content

Accessibility Standards

Accessibility Standards

Semantic HTML

Use proper HTML elements:

// Good - semantic HTML
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

<main>
  <article>
    <header>
      <h1>Article Title</h1>
    </header>
    <section>...</section>
  </article>
</main>

// Bad - divs everywhere
<div className="nav">
  <div><div onClick={...}>Home</div></div>
</div>

ARIA Labels

Add ARIA attributes where semantic HTML isn't enough:

// Icon buttons need labels
<button aria-label="Close dialog">
  <XIcon />
</button>

// Form errors
<input
  aria-invalid={!!error}
  aria-describedby={error ? 'email-error' : undefined}
/>
{error && <p id="email-error" role="alert">{error}</p>}

// Loading states
<div aria-busy={isLoading} aria-live="polite">
  {isLoading ? <Spinner /> : content}
</div>

Keyboard Navigation

Ensure all interactive elements are keyboard accessible:

// Custom interactive elements need keyboard handlers
function Card({ onClick }: { onClick: () => void }) {
  return (
    <div
      role="button"
      tabIndex={0}
      onClick={onClick}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onClick();
        }
      }}
    >
      ...
    </div>
  );
}

// Focus management in modals
function Modal({ isOpen, onClose }: ModalProps) {
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (isOpen) {
      closeButtonRef.current?.focus();
    }
  }, [isOpen]);

  // Trap focus within modal...
}

Testing Accessibility

Use getByRole in tests:

// Good - query by role
const submitButton = screen.getByRole('button', { name: /submit/i });
const emailInput = screen.getByRole('textbox', { name: /email/i });
const errorMessage = screen.getByRole('alert');

// Avoid - query by test ID
const submitButton = screen.getByTestId('submit-btn'); // Last resort