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;
}