← Back to Writing
5 min read

Writing Accessible React Components


React doesn't make your app accessible. It gives you the tools to build accessible UIs, but the accessibility work is still yours. These are the patterns I reach for most.

Use the Right Element

Start with the right HTML. A button component should render a <button>. A link component should render an <a>. This sounds obvious, but component abstractions frequently get this wrong — especially when the same component needs to behave as either a button or a link depending on context.

interface ActionProps {
  href?: string;
  onClick?: () => void;
  children: ReactNode;
}

export function Action({ href, onClick, children }: ActionProps) {
  if (href) {
    return <a href={href}>{children}</a>;
  }
  return <button onClick={onClick}>{children}</button>;
}

Managing Focus in Modals

When a dialog opens, focus must move inside it and be trapped there. When it closes, focus returns to the trigger. The inert attribute is now well-supported and is the cleanest way to trap focus:

useEffect(() => {
  if (!open) return;
  const previouslyFocused = document.activeElement as HTMLElement;
  dialogRef.current?.focus();

  return () => {
    previouslyFocused?.focus();
  };
}, [open]);

Live Regions for Dynamic Content

When content updates without a page reload — a form submission confirmation, a cart count change, a toast notification — screen readers don't announce it unless you explicitly tell them to.

<div aria-live="polite" aria-atomic="true" className="sr-only">
  {statusMessage}
</div>

aria-live="polite" waits for the user to finish their current interaction before announcing. aria-live="assertive" interrupts immediately — use only for genuine errors or time-sensitive information.

The sr-only Utility

Some content should be available to screen readers but not visible. A classic example: icon-only buttons need a text label that assistive technology can read.

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}