Accessible component libraries survey
which ones actually pass an axe audit
We installed seven of the most-downloaded React component libraries shipping in 2026, wired each one into a fresh Next.js 15 / React 19 / TypeScript app, and ran the same audit harness against every primitive: axe-core 4.11 in headless Chromium, manual keyboard sweeps, NVDA on Windows, VoiceOver on macOS, and a Lighthouse-accessibility pass for the bundle-size cost.
1. The audit harness
Every library was installed into the same React 19 / Next.js 15 / TypeScript 5.5 scaffold using its current vendor-recommended install path: npm i for the packaged libraries (Radix UI primitives, Headless UI 2.x, Mantine 8.x, Chakra UI v3, Ark UI, React Aria Components), and the shadcn CLI for shadcn/ui — which copies source into components/ui rather than vendoring a package. We then mounted a kitchen-sink demo page exposing each library’s full set of interactive primitives in their default, untouched state, with no theme overrides and no custom wrappers.
The audit harness ran in three passes. Pass one was an automated axe-core 4.11 sweep through Playwright against the rendered DOM, with the full WCAG 2.2 AA rule set and the experimental rules enabled. Pass two was a manual keyboard sweep: tab, shift-tab, arrow keys, escape, enter, space, and home/end against every primitive, scored against the WAI-ARIA Authoring Practices Guide expected keyboard contract. Pass three was a screen-reader smoke test with NVDA 2025.1 on Chrome and Safari/VoiceOver on macOS Sonoma, looking for the role announcement, the accessible name, and the state announcement when the user interacts.
We picked eleven ARIA patterns as the surface area for the matrix because these are the patterns that show up in product UIs that get sued: dialog, alert dialog, combobox, listbox, menu, menubar, tabs, accordion, tooltip, switch, and slider. A library that nails buttons and headings but ships a broken combobox is a library that will fail an audit the first time a real user tries to filter a customer list.
An axe-pass means zero violations of any rule in the WCAG 2.2 AA tag plus the experimental tag, rendered with default props and the library’s documented usage example. It does not mean the library is bulletproof — axe catches roughly half of all WCAG failures — but a library that cannot pass axe in its own demo cannot pass anywhere else.
”The default state is the only state most engineering teams ever see. If a library ships a broken default, the broken default ships to production.”
2. Seven libraries, side by side
Three of the seven libraries — Radix UI, React Aria Components, and Ark UI — passed axe on every primitive in their default state with no overrides required. Headless UI passed on all but the menu primitive, where a missing aria-activedescendant on the listbox-style menu trigger threw a single violation. shadcn/ui — which is itself a thin layer on Radix UI — passed on every primitive it ships, but the catch is that it only ships about two-thirds of the Radix surface, and the gaps (combobox, listbox, menubar) are exactly the patterns where vendors most often get accessibility wrong when they re-roll them by hand.
Mantine and Chakra UI v3 were the two libraries where the defaults shipped axe violations. Mantine’s combobox, switch, and slider all generated at least one axe violation in their documented usage example, mostly around missing accessible names on the underlying input. Chakra UI v3 — which moved to a Zag-based state-machine architecture in late 2024 — fixed many of the v2 issues but still ships a tooltip that triggers on hover only, which is a 1.4.13 Content on Hover or Focus violation in axe and a keyboard-trap risk in screen-reader virtual mode.
3. Pattern coverage matrix
The eleven-pattern grid below is the headline reference. A green cell means the library ships the primitive natively and passes axe in its default state. A yellow cell means the library ships the primitive but at least one axe violation, keyboard-contract gap, or screen-reader gap appeared in the documented usage example. A grey “N/A” means the library does not ship that primitive — for shadcn/ui that’s three patterns where you would have to import Radix directly or wire a third-party.
| Pattern | Radix | React Aria | Ark UI | shadcn | Headless | Mantine | Chakra v3 |
|---|---|---|---|---|---|---|---|
| Dialog | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| Alert dialog | Pass | Pass | Pass | Pass | Pass | Partial | Pass |
| Combobox | Pass | Pass | Pass | N/A | Pass | Partial | Pass |
| Listbox | Pass | Pass | Pass | N/A | Pass | Pass | Pass |
| Menu | Pass | Pass | Pass | Pass | Partial | Pass | Pass |
| Menubar | Pass | Pass | Pass | N/A | N/A | Pass | Pass |
| Tabs | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| Accordion | Pass | Pass | Pass | Pass | Pass | Pass | Pass |
| Tooltip | Pass | Pass | Pass | Pass | Pass | Pass | Partial |
| Switch | Pass | Pass | Pass | Pass | Pass | Partial | Pass |
| Slider | Pass | Pass | Pass | Pass | Pass | Partial | Pass |
A grey N/A cell is not a failure — it is a sourcing question. The shadcn/ui story is that you copy in the source you need from a curated set, then ship; for the three N/A patterns you either import a Radix primitive directly or pull from a sister registry. The risk in shadcn/ui is not the components it ships — those inherit Radix’s compliance — it is the components it does not ship, where teams end up hand-rolling a combobox at 1am to meet a deadline.
4. Keyboard contracts that broke
The most repeatable failure across libraries was not a missing aria attribute — it was a keyboard contract that almost matched the APG and then diverged by one key. Mantine’s combobox does not respond to Home and End as the APG specifies. Chakra v3’s tooltip cannot be dismissed by Escape when it was opened by hover. Headless UI’s menu primitive collapses on the first ArrowDown rather than placing focus on the first item, because the implementation treats the trigger as the active descendant on open.
These are not exotic edge cases. They are the patterns a screen-reader user reaches for on minute one. The comparison below pairs the same combobox API written two ways — once with Mantine’s defaults, once with React Aria Components — to show what the keyboard contract looks like when it is treated as a spec rather than as a polish item.
<Combobox
store={combobox}
onOptionSubmit={(v) => setValue(v)}
>
<Combobox.Target>
<InputBase
// no aria-controls wiring on the input
// Home/End keys do not move focus inside the listbox
// ArrowDown opens but does not place focus on item 1
value={value}
onChange={(e) => setValue(e.currentTarget.value)}
/>
</Combobox.Target>
<Combobox.Dropdown>
{/* listbox items render here */}
</Combobox.Dropdown>
</Combobox><ComboBox aria-label="Filter customers">
<Label>Customer</Label>
<Input />
<Popover>
<ListBox>
<ListBoxItem>Alpha</ListBoxItem>
<ListBoxItem>Bravo</ListBoxItem>
<ListBoxItem>Charlie</ListBoxItem>
</ListBox>
</Popover>
</ComboBox>
// aria-controls, aria-activedescendant,
// aria-expanded, role=combobox, Home/End,
// PageUp/PageDown, Escape: all wired by default.Libraries that derive their keyboard contracts from a published spec — React Aria from the WAI-ARIA APG, Ark UI from the Zag.js state machines — ship those contracts as the only behaviour the component supports. Libraries that treat the keyboard contract as a feature list ship roughly 80% of it and leave the last 20% as “future work.” That last 20% is exactly the keys assistive-tech users press most.
5. Bundle-size cost of accessibility
The most common objection to picking the strict libraries is bundle size. The audit did not bear this out. Radix UI primitives are tree-shakeable per-primitive and ship in the approx. 7-15 KB gzipped range each; React Aria Components is heavier — approx. 95 KB gzipped for the full bundle — but is also tree-shakeable. Headless UI is the lightest at approx. 25 KB gzipped for the entire package. Mantine and Chakra v3 are both batteries-included and ship the styling system in the same bundle, putting them in the approx. 180-220 KB gzipped range out of the box.
The trade-off is real but smaller than the discourse suggests. A combobox that does not respect Home and End costs approximately the same bytes as one that does. The choice is not “accessibility versus performance” — it is “spec-derived behaviour versus hand-rolled behaviour,” and the hand-rolled version is more often the bigger one because it carries its own ad-hoc state machine.
6. Playbook for picking a stack
If the product is an enterprise app with a procurement-led accessibility requirement, pick React Aria Components.
It is the only library audited whose APIs are explicitly derived from the WAI-ARIA Authoring Practices Guide. The bundle is heavier than Radix, but the audit-pass story to a VPAT reviewer is the cleanest because every primitive has a published conformance claim.
If the team owns its design system, pick Radix UI (and optionally shadcn/ui on top of it).
Radix gives unstyled, spec-conformant primitives. shadcn/ui makes them easy to copy in and theme. The thing to watch is the three patterns shadcn does not ship — combobox, listbox, menubar — for which you should import Radix directly rather than hand-roll.
If the team is Tailwind-first and only needs a narrow surface, pick Headless UI — but audit the menu primitive in your own code.
Headless UI is the smallest bundle in the field and ships a small, well-tested surface. The one menu-primitive gap is documented above; fix it with an explicit aria-activedescendant wire on the listbox-style menu, or wrap the menu with a project-local helper that does so.
If the team values batteries-included over spec-conformance, pick Mantine or Chakra v3 — but plan for overrides.
Both libraries are fast to ship with and look good out of the box. Both also require per-component overrides to clear the audit on combobox, switch, slider (Mantine) or tooltip (Chakra v3). Budget the override work into the sprint that adopts the library, not the sprint that fails the audit.
Run axe against the library’s own demo before adopting it.
Vendors maintain demo sites. Point axe DevTools at them. If the vendor’s own demo throws violations on the primitives you plan to use, those violations will follow into your product. This single five-minute check separates the libraries you adopt from the ones you avoid.
Conclusion: the spec is the contract
The libraries that clear an axe audit cleanly are the libraries whose authors treated the WAI-ARIA Authoring Practices Guide as a contract rather than a reference. Radix UI, React Aria Components, and Ark UI each derive their keyboard contracts and ARIA wiring from a published spec — APG in the case of Radix and React Aria, the Zag.js state machines in the case of Ark UI — and they ship the spec, not a subset of it.
The libraries that fail are not failing because their authors do not care about accessibility. They are failing because the keyboard contract was treated as a feature list, and feature lists end up at 80% rather than 100%. The last 20% — Home and End in a combobox, Escape to dismiss a hover-opened tooltip, focus management on the first ArrowDown in a menu — is the part the user notices.
The shadcn/ui story sits oddly across both categories. The components it ships inherit Radix’s spec-derivation and pass. The components it does not ship are the gaps where teams hand-roll an in-house combobox, and that in-house combobox is where the next axe violation enters the codebase. The fix is not to pick a different library — it is to import Radix directly for those three patterns and treat them with the same seriousness as the rest of the design system.
Engineering teams adopting a library should pair the choice with the rest of the developer toolkit at our developers landing, run a free WCAG 2.2 scan on the library’s demo before shipping it into production, and benchmark the result against the full WCAG 2.2 success-criteria reference.
”Pick the library whose authors treated the spec as a contract, and the audit becomes a formality. Pick the library whose authors treated the spec as a reference, and the audit becomes a backlog.”