Why Your Custom Components Fail Accessibility Audits (And How Radix UI Fixes It)
Run an accessibility audit on most custom-built component libraries and you'll find the same issues: dropdowns that can't be navigated with arrow keys, modals that don't trap focus, tooltips that screen readers ignore, and menus that can't be dismissed with Escape.
These aren't edge cases. They're the baseline for accessible UI.
What Accessibility Actually Requires
Take a dropdown menu. A visual-only implementation needs: a trigger button, a list of options, and click handlers. Maybe 30 lines of code.
An accessible implementation needs:
role="menu"on the list,role="menuitem"on each optionaria-expandedon the trigger, toggled on open/closearia-haspopup="menu"on the trigger- Arrow key navigation (up/down to move, Enter to select, Escape to close)
- Home/End keys to jump to first/last item
- Typeahead — typing "s" jumps to the first item starting with "s"
- Focus trapping inside the menu when open
- Focus restoration to the trigger when closed
- Click-outside dismissal without breaking focus flow
- Screen reader announcement when the menu opens
That's 200+ lines of interaction logic for one component. Multiply by every interactive pattern in your app — dialog, tooltip, tabs, accordion, select, popover, combobox — and you're maintaining thousands of lines of behavior code.
The Headless Primitive Approach
Radix UI (and similar libraries like Ariakit and React Aria) ship behavior without visual opinions:
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
<DropdownMenu.Root>
<DropdownMenu.Trigger>Options</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item>Edit</DropdownMenu.Item>
<DropdownMenu.Item>Duplicate</DropdownMenu.Item>
<DropdownMenu.Item>Delete</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
Radix handles: ARIA attributes, keyboard navigation, focus management, typeahead, click-outside, screen reader announcements. You handle: how it looks.
This is the right separation. Interaction behavior is hard to get right and rarely needs customization. Visual presentation is easy to get right and always needs customization.
The Patterns That Matter Most
Focus-Visible vs Focus
/* Shows focus ring on mouse clicks — annoying for mouse users */
button:focus { outline: 2px solid blue; }
/* Shows focus ring only on keyboard navigation — correct */
button:focus-visible { outline: 2px solid blue; }
focus-visible is a CSS pseudo-class that activates only on keyboard interaction. Mouse users don't see focus rings. Keyboard users do. This is the correct behavior per WCAG.
Skip Navigation
Screen reader users and keyboard navigators shouldn't tab through your entire header and navigation on every page load. A skip link jumps them to the main content:
<a href="#main-content" className="sr-only focus-visible:not-sr-only">
Skip to main content
</a>
Visually hidden by default, visible when focused via keyboard. Simple, impactful.
Reduced Motion
Users with vestibular disorders can set prefers-reduced-motion: reduce at the OS level. Your animations should respect this:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Or per-component with Framer Motion's useReducedMotion() hook.
Consistent Disabled States
Every disabled element should look the same: opacity: 0.5 and cursor: not-allowed. Don't vary disabled opacity across components — it creates visual inconsistency and confuses users about what's interactive.
The Business Case
Accessibility isn't a feature checkbox:
- Legal — the European Accessibility Act (EAA) takes effect in 2025. ADA lawsuits in the US have increased 300% since 2018.
- Market — 1.3 billion people worldwide have a disability. That's 16% of the global population.
- SEO — semantic HTML with proper ARIA attributes improves crawlability and can improve search rankings.
- Quality signal — accessible code is well-structured code. Teams that ship accessible products ship fewer bugs overall.
The Audit Checklist
Before shipping any interactive component:
| Check | Tool |
|---|---|
| Keyboard navigation works | Tab through it manually |
| Screen reader announces correctly | VoiceOver (Mac) or NVDA (Windows) |
| Focus is visible and trapped where needed | Tab + visual inspection |
| Color contrast meets 4.5:1 for text | Chrome DevTools or axe |
| Reduced motion is respected | System settings + visual check |
| Semantic HTML is used | `button` for actions, `a` for links |
Building accessible UI from scratch is a full-time job. Using headless primitives that handle the behavioral layer lets you focus on the visual layer — where your product's identity actually lives.