Skip to content

Accessible UI Libraries Compared

Compare accessible UI kits on a11y guarantees, styling escape hatches, and ownership cost, pick on axes, not logos.

5 min read
Accessibility
Radix UI
React Aria
Headless UI
Base UI
Design System

Primitives belong in a versioned package. Most teams should not hand-roll every focus trap. Pick the accessible primitive layer you buy, and spell out what you still owe after install.

I compare libraries on three axes: a11y guarantees, styling escape hatches, and bundle / ownership cost. Logo loyalty is not an axis.

The cast (mid-2025)

LibraryModelYou own styling?
Radix UI PrimitivesUnstyled behavior primitivesYes: compose classes/tokens
React Aria / React SpectrumBehavior hooks + optional Spectrum chromeHooks: yes; Spectrum: opinionated
Headless UIUnstyled components (Tailwind-adjacent docs)Yes
Base UI (MUI Base lineage)Unstyled primitivesYes
shadcn/ui-style kitsCopy-paste composed components on primitivesYou own the source in-repo

I am not crowning a winner. I am naming what you are actually purchasing.

Axis 1: Accessibility guarantees

Ask: what is tested and documented, keyboard, focus, ARIA roles, and screen reader expectations?

Radix and React Aria both treat WAI-ARIA patterns as the product. Dialogs move focus, restore focus, and handle Escape. Tabs implement roving tabindex. That baseline is what you are buying.

Headless UI covers common patterns well; depth varies by component, read the docs for the control you need, don't assume parity with Radix's full set.

Copy-paste kits (shadcn-style) inherit whatever primitive they wrap. Your guarantee is only as good as (a) the upstream primitive and (b) whether your fork still wires aria-* after restyles.

tsx
// illustrative: Radix Dialog composition with tokens import * as Dialog from '@radix-ui/react-dialog'; export function ConfirmDialog({ open, onOpenChange, title, children, }: { open: boolean; onOpenChange: (open: boolean) => void; title: string; children: React.ReactNode; }) { return ( <Dialog.Root open={open} onOpenChange={onOpenChange} > <Dialog.Portal> <Dialog.Overlay className="acme-overlay" /> <Dialog.Content className="acme-dialog" aria-describedby={undefined} > <Dialog.Title>{title}</Dialog.Title> {children} <Dialog.Close className="acme-btn">Close</Dialog.Close> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ); }

If you replace Dialog.Title with a styled <div>, you silently deleted the accessible name. Libraries cannot save you from that.

Axis 2: Styling escape hatches

Unstyled primitives win when design tokens (May) and themes (June) are real. You attach classes or data attributes; the library stays out of your visual system.

Spectrum (and other full design languages) win when you want a coherent Adobe-like UI fast and will accept their visual gravity.

Escape-hatch failure modes:

  • Fighting a styled kit with !important and wrapper divs
  • Re-implementing padding in three layers because the kit ships opinions and your tokens disagree
  • Choosing "fully custom" then shipping inaccessible custom selects anyway
tsx
// React Aria hook-style, behavior without chrome (illustrative) import { useToggleButton } from 'react-aria'; import { useToggleState } from 'react-stately'; import { useRef } from 'react'; export function MuteToggle() { const state = useToggleState(); const ref = useRef<HTMLButtonElement>(null); const { buttonProps, isSelected } = useToggleButton({}, state, ref); return ( <button {...buttonProps} ref={ref} data-selected={isSelected} > {isSelected ? 'Muted' : 'Sound on'} </button> ); }

Hooks maximize styling freedom. They also maximize you must compose correctly: there is no Storybook default to hide mistakes.

Axis 3: Bundle and ownership cost

ApproachBundle shapeOwnership
Per-primitive packages (Radix-style)Pay for what you importUpstream a11y + your styles
Monolithic headlessTree-shake quality variesSame
Copy-paste kitCode lives in your repoYou merge upstream by hand
Full design systemLarger, coherentTheme tokens vs their theme

Copy-paste is not free. You own diffs, a11y regressions, and dependency bumps for the underlying primitive. That is often the right trade for a single product with a strong design hand, and the wrong trade if nobody reviews focus behavior when "just changing the border radius."

Decision rule: if you need three dialogs and a select, start with primitives + tokens. If you need a full enterprise chrome tomorrow and design bandwidth is near zero, a complete system may be cheaper than a fake custom kit.

What I still build myself

Even with a strong primitive layer, I still own:

  • Domain components (primitives-to-domain post)
  • Layout shells and navigational patterns (responsive layout post)
  • Product empty states and error copy
  • Any control the library does not cover, with an a11y checklist before merge

Buying primitives is not outsourcing responsibility. It is refusing to re-implement aria-modal incorrectly for the fifth time.

Continuity

Primitives versus domain components set the package split. Accessible libraries are the behavior substrate under those primitives. Responsive layout, sidebar and mobile patterns follow, where CSS often beats extra component chrome.

Takeaway

Pick accessible libraries on guarantees, styling freedom, and who owns the diffs, not on which logo your last team used.