Headless UI components split behavior from presentation. A headless component handles the accessible logic, keyboard interaction, and state of a widget, and ships no visual styling, so you build the look yourself. Radix Primitives is the canonical example.
Behavior without a look
A styled component library gives you a finished button or dialog. A headless library gives you the working dialog logic and lets you paint every part. The tradeoff is more markup up front, in exchange for a widget that matches your design system exactly.
That separation matters because accessibility is the hard half. Focus management, ARIA wiring, and keyboard behavior are easy to get wrong and tedious to rebuild, while a rounded corner or a shadow is easy to write yourself.
How a headless component works
Radix Primitives ship as the radix-ui package. Install it and import the widgets you need.
npm install radix-uiEach primitive exposes a small tree of parts. A tooltip, for example, has a trigger and floating content, and Radix handles the hover, focus, and positioning behavior between them.
import { Tooltip } from "radix-ui";
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button>Save</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content>Save your changes</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>None of these parts carry a color or font. The asChild prop renders the trigger through your own button, so you keep full control of the element while Radix keeps the tooltip behavior and accessibility.
Style it yourself
Styling is just className on each part. The same tooltip with Tailwind utilities looks like this.
<Tooltip.Content className="rounded bg-gray-900 px-2 py-1 text-xs text-white">
Save your changes
</Tooltip.Content>The gray background and padding are entirely yours. Radix still positions the content next to the trigger, dismisses it on Escape and blur, and adds the correct ARIA relationships, none of which you had to reimplement.
Why this beats rebuilding widgets
Building a dialog from scratch means focus trapping, ARIA, and keyboard handling, which is exactly the work a headless library already did and tested. Using one removes that entire category of bugs from your components.
When to use headless components
Reach for a headless library when you need a complex widget and your team wants a custom look. A dialog, menu, or select is exactly the kind of component where the accessibility is hard and the styling is easy to own. The library ships the hard part, and you keep the visual part.
Skip it for simple elements. A plain button or input already carries the behavior and semantics, and wrapping it in a primitive adds a dependency without benefit.
The primitive's internal structure is the compound components pattern at scale, and the hand built version of one such widget is the accessible tooltip walkthrough.
Rune AI
Key Insights
- Headless components ship behavior without styles.
- Radix Primitives is the canonical unstyled component library.
- Install the radix-ui package and import the primitives you need.
- Style each part with your own className values.
- Use asChild to render a primitive through your own element.
Frequently Asked Questions
What is the difference between headless and styled component libraries?
Do headless components work with Tailwind?
Is Radix the only headless library?
Conclusion
Headless UI components separate behavior from presentation. They handle ARIA, focus, and keyboard interactions while leaving every visual decision to you, which is why libraries like Radix form the base of many design systems.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Compound Components in React: Build Flexible UI APIs
Build flexible React component APIs with the compound components pattern. Share state through context and let users arrange the parts.