11 — Headless Component Libraries — Logic Without a Look
"Components that look plain" was my headless-library misunderstanding, and it missed the point entirely. The model that fixed it: a headless library gives you the behavior, state management, and accessibility wiring of a component, with zero styling. You provide the look; the library provides everything you'd get wrong by hand. [1] That split — behavior separate from appearance — is what makes it possible to build a fully custom, on-brand UI that's still keyboard-navigable, screen-reader-friendly, and correct.
The problem headless solves
Interactive components — dialogs, dropdowns, comboboxes, tooltips, tabs — are hard for reasons that have nothing to do with how they look:
- Focus management. A dialog must trap focus inside itself while open and restore it to the trigger when closed. Get this wrong and keyboard users are stranded.
- Keyboard navigation. Arrow keys, Enter, Escape, Tab — every component has an expected keyboard contract defined by WAI-ARIA.
- ARIA attributes. The right roles, states, and labels have to be set on the right elements so screen readers announce the component correctly.
- State coordination. Which option is highlighted, is the menu open, what's the active descendant — these pieces have to stay in sync.
Building all of that correctly, per component, is weeks of accessibility work. A styled library (MUI, Chakra) handles it but locks me into its look. A headless library handles it and hands me unstyled primitives I dress up myself [1][2].
Radix UI: the unstyled primitives
Radix UI is the canonical headless library for React [3]. It provides a range of unstyled, fully accessible primitives — each component is split into parts (Trigger, Content, Overlay, etc.) that I compose with my own markup and styles.
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="my-button">Open</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="my-overlay" />
<Dialog.Content className="my-content">
<Dialog.Title>Are you sure?</Dialog.Title>
<Dialog.Close>Cancel</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>Radix owns the focus trap, the Escape-to-close, the ARIA wiring, the portal rendering. I own every class name and DOM structure. This is the same layer Shadcn builds on top of — Shadcn is essentially "Radix primitives + opinionated Tailwind styling, delivered as source." Knowing Radix directly matters because it's what I reach for when Shadcn's defaults aren't a fit but I still want the accessibility for free.
React Aria: Adobe's behavioral hooks
React Aria (from Adobe's React Spectrum team) takes a different architectural cut [4]. Instead of component parts, it provides hooks that return the state, props, and event handlers a component needs. I apply those props to my own elements.
function MyButton() {
const { buttonProps } = useButton({ onPress: () => {} });
return <button {...buttonProps} className="my-button">Save</button>;
}React Aria's library covers the full WAI-ARIA pattern set and is rigorously tested across assistive tech. The hook-based API is more flexible than component parts (I can build any DOM structure I want), at the cost of slightly more wiring. It's also style-free out of the box, so it composes with any styling solution.
Ark UI: framework-agnostic, multi-runtime
Ark UI is a newer entry that takes the headless logic and makes it framework-agnostic — the same components work across React, Vue, and Solid [5]. Its architecture is close to Radix (machine-based state under the hood, composable parts), with an emphasis on a comprehensive component set and consistent APIs. For a project that spans multiple frameworks, or one that wants a single headless layer to standardize on, Ark UI is the choice. In a React-only project, Radix remains the more established option.
How I use this
The reason this category exists is that accessibility is non-negotiable and hard, and a custom visual design shouldn't force me to give up on it. My default flow for an interactive component is to reach for the headless primitive first (Radix via Shadcn, most commonly) and apply my own styling — I get the focus management, keyboard support, and ARIA correctness for free, and the look is fully under my control. I reach for React Aria directly when I need the hook-level flexibility to build a DOM structure the component-part APIs don't accommodate, and I'd consider Ark UI if standardizing across frameworks. What I don't do is build interactive components from raw <div>s and onClick — that path leads to dialogs that don't trap focus, dropdowns that don't open on keyboard, and the slow accumulation of accessibility debt.
References
[1] verthon, "Headless UI libraries — the key to flexible and accessible user interfaces," dev.to, 2023. [Online]. Available: https://dev.to/verthon/headless-ui-libraries-the-key-to-flexible-and-accessible-user-interfaces-546p
[2] LogRocket, "The complete guide to building headless interface components in React," blog.logrocket.com, 2023. [Online]. Available: https://blog.logrocket.com/the-complete-guide-to-building-headless-interface-components-in-react/
[3] Radix UI, "Radix UI — open-source accessible component primitives," radix-ui.com, 2024. [Online]. Available: https://www.radix-ui.com/
[4] Adobe, "React Aria — a library of accessible React hooks," react-spectrum.adobe.com, 2024. [Online]. Available: https://react-spectrum.adobe.com/react-aria/
[5] Ark UI, "Ark UI — headless components for your design system," ark-ui.com, 2024. [Online]. Available: https://ark-ui.com/
Knowledge check · Question 1 of 5
A headless component library provides:
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!