---
title: "11 — Headless Component Libraries — Logic Without a Look"
uid: headless-libraries
tags: ["accessibility", "react", "roadmap:react", "ark-ui", "radix", "headless", "react-aria"]
excerpt: "A headless library gives you the behavior, state, and accessibility of a component with zero styling — you write the look. That split makes custom-but-accessible UIs possible without rebuilding focus traps."
date: 2026-08-13T03:27:44+0000
source: https://www.aveshina.my.id/en/blog/headless-libraries
---

"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.

```figure
<svg viewBox="0 0 740 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Headless vs styled libraries. A single component split into two layers: the headless layer (Radix, React Aria, Ark UI) provides behavior, state, accessibility, keyboard nav — no CSS. The style layer (your Tailwind/CSS) provides the look. A styled library fuses both; a headless library gives you only the bottom layer.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- style layer (top) -->
    <rect x="80" y="40" width="580" height="80" rx="10" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="370" y="64" font-size="13" font-weight="700" fill="#500724" text-anchor="middle">Style layer — your Tailwind / CSS</text>
    <text x="370" y="84" font-size="11" fill="#500724" text-anchor="middle">class names · DOM structure · visual design</text>
    <text x="370" y="104" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">a styled library bakes this in; a headless library leaves it to you</text>

    <!-- behavior layer (bottom) -->
    <rect x="80" y="140" width="580" height="100" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="370" y="164" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">Behavior layer — Radix · React Aria · Ark UI</text>
    <text x="370" y="186" font-size="11" fill="#1e1b4b" text-anchor="middle">focus trap · keyboard nav · ARIA roles · state · portals</text>
    <text x="370" y="206" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">the part that's weeks of accessibility work if you build it by hand</text>
    <text x="370" y="226" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">delivered unstyled — you compose it with your own markup</text>

    <!-- brace -->
    <path d="M680,80 C700,80 700,190 680,190" fill="none" stroke="#475569" stroke-width="1.5"/>
    <text x="710" y="138" font-size="11" fill="#475569" text-anchor="middle" font-style="italic" transform="rotate(90 710 138)">composed into one component</text>
  </g>
</svg>
```

## 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](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/](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/](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/](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/](https://ark-ui.com/)

```quiz
Q: A headless component library provides:
- finished, styled components ready to import
- the behavior, state, and accessibility wiring of a component, with no styling
- a CSS framework for styling
correct: 1
explain: Headless libraries own the hard parts — focus management, keyboard nav, ARIA, state — and hand you unstyled primitives. You write the look; they handle the correctness you'd otherwise get wrong.

Q: Why is focus trapping in a dialog something a headless library handles for you?
- because doing it by hand is error-prone and strands keyboard users if missed
- because React can't render dialogs otherwise
- to make the dialog animate smoothly
correct: 0
explain: A dialog must keep focus inside itself while open and restore it to the trigger on close. Getting this wrong is an accessibility failure, so headless libraries (Radix, React Aria) implement it correctly.

Q: Radix UI's architecture is:
- split each component into composable parts (Trigger, Content, Overlay) you style yourself
- a single sealed <Widget> per component
- a CSS reset
correct: 0
explain: Radix primitives are composed of parts; you wrap your own markup and classes around them. Shadcn is built on top of this, adding opinionated Tailwind styling.

Q: React Aria's API shape is:
- hooks that return props/state you spread onto your own elements
- styled <Button> components
- a Tailwind plugin
correct: 0
explain: React Aria exposes behavioral hooks (useButton, useDialog) returning props and handlers. That's more flexible than component parts — you can use any DOM structure — at the cost of slightly more wiring.

Q: Shadcn UI's relationship to Radix is:
- Shadcn is built on Radix primitives, adding opinionated Tailwind styling delivered as source
- Shadcn replaces Radix entirely
- Radix is built on Shadcn
correct: 0
explain: Shadcn = Radix (headless behavior) + opinionated Tailwind styling, copied into your repo. Knowing Radix directly matters for cases where Shadcn's defaults don't fit but you still want the accessibility.
```
