Headless UI Libraries in 2026: Radix, Headless UI, Ark Compared
Radix, Headless UI, and Ark UI compared side-by-side in 2026 — accessibility defaults, bundle size, DX, and which one you should actually pick for your next React project.
Why Headless UI Even Exists
The premise is simple enough: component libraries got tired of shipping opinions about colour and spacing, so they stripped everything down to just behaviour and accessibility. You get the Dialog that traps focus, handles Escape, and manages ARIA attributes — but the 8px border-radius and the background: #1a1a2e are yours to own. That shift happened in earnest around 2021 and by 2026 it's the default expectation on any greenfield React project.
The problem is you now have three credible options and picking wrong means either rewriting your dialog logic in six months or fighting an API that doesn't map to your design system. Radix UI, Tailwind's Headless UI, and Ark UI have genuinely different philosophies. They're not interchangeable.
Honestly, most comparison posts treat them as "just pick one" — but bundle size differences hit 40kb+ in real projects, and API surface gaps bite you the moment you try to build a custom combobox with async search. Worth thinking through before you npm install.
One more thing — if you're building something heavily styled rather than building a pure component lib, you might want pre-built visual components from a library like Empire UI alongside your headless primitives. The two approaches aren't mutually exclusive.
Radix UI: The Safe Default
Radix has been the de facto standard since shadcn/ui adopted it as its primitive layer in 2023. By 2026, it's essentially the React headless default — you'll find it in practically every starter kit, opinionated or not. If you open a new Next.js app and reach for components, Radix is probably already in your dependency tree transitively.
The API is compositional in the extreme. A Dialog is Dialog.Root, Dialog.Trigger, Dialog.Portal, Dialog.Overlay, Dialog.Content, Dialog.Title, Dialog.Close — seven components for one dialog. That reads as boilerplate until you realise it gives you surgical control. You can portal the overlay to document.body while keeping content inside your layout tree. You can swap the trigger for any element. Granularity is the point.
import * as Dialog from '@radix-ui/react-dialog';
export function ConfirmModal({ onConfirm }: { onConfirm: () => void }) {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="btn-danger">Delete</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-surface rounded-xl p-6 w-[480px]">
<Dialog.Title className="text-lg font-semibold">Confirm delete?</Dialog.Title>
<div className="mt-4 flex gap-2 justify-end">
<Dialog.Close asChild>
<button className="btn-ghost">Cancel</button>
</Dialog.Close>
<button className="btn-danger" onClick={onConfirm}>Delete</button>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}The accessibility story is excellent. ARIA roles, aria-modal, focus trapping, scroll locking — all handled by default with sensible escape hatches. The only real gripe in 2026 is that Radix's bundle is per-package, so @radix-ui/react-dialog is ~12kb gzipped but if you pull in ten primitives you're looking at 80–100kb total. Tree-shaking at the package level, not the module level.
That said, the DX is genuinely polished. TypeScript types are accurate, the docs are thorough, and the asChild pattern (where a component forwards all its props and behaviour onto its child rather than rendering its own DOM element) is one of the cleverest API decisions in the React ecosystem. Worth knowing: asChild is the thing that makes shadcn/ui feel so flexible.
Headless UI: Minimal Surface, Tailwind First
Headless UI (the one from the Tailwind Labs team, @headlessui/react) has a very different philosophy. Fewer components, smaller scope, and a clear assumption that you're using Tailwind CSS for styling. As of v2.1 in 2025 it added the transition integration that maps directly onto Tailwind's transition utilities, which made the DX noticeably smoother if you're already in that ecosystem.
The component count is lower — Combobox, Dialog, Disclosure, Listbox, Menu, Popover, RadioGroup, Switch, Tab, Transition. That's it. Radix ships closer to 30 primitives. If you need a Tooltip or a ContextMenu or a Toast, Headless UI isn't your library. In practice, this is either a dealbreaker or completely irrelevant depending on your project.
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
export function SimpleModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/40" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md bg-white rounded-2xl p-6 shadow-xl">
<Dialog.Title className="text-lg font-bold">Modal Title</Dialog.Title>
<button onClick={onClose} className="mt-4 btn">Close</button>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
}Look, the transition API is verbose. You can see it in the code above — wrapping each animated layer in its own Transition.Child with enter/leave class strings is fine until you have five layers and you're managing six sets of class strings. Radix handles animation via data attributes (data-state="open") which you target with CSS, and honestly that scales better.
Quick aside: Headless UI v2.x added a React 19 compatible render prop model and the bundle is genuinely small — around 18kb gzipped for the whole library. If you're building a Tailwind-native project with modest component needs, it's the leanest option on this list.
Ark UI: The Dark Horse Worth Watching
Ark UI came out of the Chakra UI team (via Zag.js, their state machine library) and it's been quietly maturing since 2023. By 2026, it's the most feature-complete option — 50+ components including date pickers, file uploads, timers, color pickers, and tour/onboarding flows. Things Radix doesn't touch.
The architecture is different under the hood. Ark uses finite state machines (via Zag) to model component behaviour. This sounds academic until you hit an edge case — like a date picker that needs to stay open when the user clicks a calendar day while holding Shift for range selection. State machines handle these branching interaction graphs without the useEffect spaghetti that typically follows. In practice, complex interactive components are noticeably more correct.
import { DatePicker } from '@ark-ui/react';
export function TaskDeadlinePicker({ onChange }: { onChange: (date: string) => void }) {
return (
<DatePicker.Root onValueChange={(details) => onChange(details.valueAsString[0])}>
<DatePicker.Label className="text-sm font-medium text-muted">Deadline</DatePicker.Label>
<DatePicker.Control className="flex gap-2">
<DatePicker.Input className="input" />
<DatePicker.Trigger className="btn-icon">
<CalendarIcon />
</DatePicker.Trigger>
</DatePicker.Control>
<DatePicker.Positioner>
<DatePicker.Content className="calendar-popup">
<DatePicker.View view="day">
<DatePicker.ViewControl>
<DatePicker.PrevTrigger>Prev</DatePicker.PrevTrigger>
<DatePicker.ViewTrigger><DatePicker.RangeText /></DatePicker.ViewTrigger>
<DatePicker.NextTrigger>Next</DatePicker.NextTrigger>
</DatePicker.ViewControl>
<DatePicker.Table>
<DatePicker.TableBody>
{/* day cells */}
</DatePicker.TableBody>
</DatePicker.Table>
</DatePicker.View>
</DatePicker.Content>
</DatePicker.Positioner>
</DatePicker.Root>
);
}The tradeoff is bundle size and API verbosity. Ark ships Zag as a dependency, which adds weight, and the compositional API is even more granular than Radix. The DatePicker alone has about 20 sub-components. If you're coming from Radix and find its seven-piece Dialog excessive, Ark is going to feel like a lot.
That said, the framework support is real — Ark works with React, Vue, and Solid from the same state machine core. If you're building a design system that needs to ship across frameworks, that's a genuine advantage no other option here offers. Worth noting: the accessibility in Ark's complex widgets (date pickers, tree views, color pickers) is significantly better than anything you'd hand-roll.
DX, Bundle Size, and When to Pick Which
Let's put some numbers to it. Radix's per-component packages average 8–15kb gzipped each. A realistic app pulling Dialog, Dropdown, Tooltip, Select, and Tabs is around 60kb before your styling code. Headless UI's entire package is ~18kb gzipped. Ark UI with Zag bundled is 45–70kb depending on which components you pull — but you're getting a date picker and a color picker in that budget, which Radix can't offer at all.
The DX gap mostly comes down to iteration speed. Radix's asChild pattern and data-attribute animation approach map cleanly onto any CSS-in-JS or utility solution. Headless UI's Tailwind-class transition system is fast when you're building quickly but painful to debug in complex animations. Ark's state machine model means fewer bugs at the cost of a steeper mental model upfront.
In practice, here's the breakdown: pick Radix if you're building on shadcn/ui or want the largest community, most examples, and predictable long-term maintenance. Pick Headless UI if you're fully committed to Tailwind and your component needs are straightforward — a marketing site or a simple dashboard. Pick Ark if you need a date picker, file upload, or any of those complex interactive widgets without writing your own state machine.
One more thing — none of these libraries tells you what your UI should look like. If you want the visual inspiration side sorted before you write a line of component code, browsing Empire UI's component library gives you a concrete target to build towards. The glassmorphism components in particular pair well with Radix's raw primitives — you supply the blur and the translucency, Radix handles focus trapping.
And if you're hunting for alternatives to shadcn/ui that use these same primitives, there's a solid breakdown in our best React UI libraries guide for 2026 that's worth reading alongside this one.
Accessibility: The Real Differentiator
All three libraries claim WCAG 2.1 AA compliance. How they get there varies. Radix is the most tested in production — it's been battle-hardened across thousands of apps since 2021, and its Dialog, Select, and Combobox implementations handle edge cases like nested modals, virtual focus in combobox lists, and screen reader announcements for live regions correctly out of the box.
Headless UI's accessibility is solid for its scope. But its Combobox in particular has had some rough edges over the years with VoiceOver on macOS — specifically around aria-activedescendant management during async search. The v2.x rewrite improved things significantly, but if accessibility in a combobox is load-bearing for your product, you'd want to test it properly before shipping.
Ark wins outright on complex widget accessibility. Date pickers are genuinely hard — ARIA patterns for calendar grids (role="grid", roving tabindex, arrow key navigation, month/year announcements) require meticulous implementation. Ark's Zag state machines encode these patterns at the state level, so the accessibility emerges from the interaction model rather than being bolted on. That's a meaningful architectural win.
The honest answer is: for simple things (Dialog, Tabs, Dropdown), all three are fine. For complex things (DatePicker, ColorPicker, Tree), only Ark ships a battle-tested solution. This is why plenty of teams mix — Radix for primitives, Ark for the complex stuff they don't want to build from scratch.
Quick Verdict
Radix is still the default choice in 2026. The ecosystem, the shadcn/ui integration, the community tooling — it's the path of least resistance and it's genuinely good. If you're starting a new project today and you're not sure which to pick, pick Radix.
Headless UI earns its place in Tailwind-first projects where you want the smallest possible footprint. Think landing pages that need a nav dropdown and a mobile drawer, not complex dashboards. The 18kb total bundle is hard to argue with for that use case.
Ark is the one to watch. The component count is highest, the accessibility is best-in-class for complex widgets, and the multi-framework support is a real differentiator for design system teams. The tradeoff is API verbosity and a steeper onboarding curve. By 2027, I'd expect Ark to be the clear recommendation for anything beyond basic CRUD.
Whichever primitive layer you land on, remember that headless is the floor, not the ceiling. You still need to design the 1px borders, the 4px border-radius, the focus rings, the motion timing. That's where visual libraries and tools like Empire UI's gradient generator or box shadow generator actually save you hours.
FAQ
For most projects, yes — Radix has a larger component set, better DX, and a massive ecosystem built around it (shadcn/ui especially). Headless UI is worth it only if you're Tailwind-first and have simple component needs.
Absolutely. Ark is completely unstyled — you wire your Tailwind classes directly onto its components just like you would with Radix. There's no conflict.
Radix and Ark both score well. For simple components (dialogs, menus), Radix is battle-tested. For complex widgets like date pickers and color pickers, Ark's state machine approach produces more correct ARIA behaviour.
Yes, as of v2.1 Headless UI is compatible with React 19 and the new concurrent rendering model. Just make sure you're on the latest patch release.