Unstyled is the right answer more often than it sounds, and less often than the people who love it suggest. A headless primitive gives you the part that takes longest and is least visible - focus management, keyboard handling, portals, collision detection, ARIA wiring - and nothing else at all. Whether that trade is good depends entirely on whether you have a design to apply.
What you are actually buying
The behaviour of a menu is not a small amount of work. Roving tabindex, typeahead, Escape returning focus to the trigger, the panel portaled out of a clipping ancestor, collision-aware positioning, and every ARIA relationship wired correctly. Multiply that by a dialog, a combobox, a select, tabs and a tooltip.
That is what a primitive library is: the answer to those problems, tested across assistive technologies, with no opinion about how any of it looks. The layers guide puts it in context, since primitives are the bottom of a three-layer stack rather than an alternative to the layer above.
When headless is right
You have a design system. Tokens, a type scale, defined states, and a designer with opinions. Then someone else's class names are an obstacle and behaviour is the only thing you want to borrow.
The design is unusual. A control that does not look like a standard control still needs to behave like one. Headless is how you get correct behaviour under an unconventional appearance.
You are building components for others. A design system team shipping to product teams wants the behaviour layer and its own visual layer, not a third party's.
When it is not
You do not have a design yet. Unstyled primitives with no design system produce a slow start and an inconsistent result, because every decision that a styled library made for you now has to be made by someone, screen by screen.
The team is small. Writing and maintaining the visual layer for twenty components is real, ongoing work. shadcn/ui exists precisely to remove it while keeping the behaviour, which is why it is the usual answer for teams that would otherwise start from primitives.
Speed matters more than distinctiveness. A styled set, adapted with tokens, gets to a coherent product much faster.
The options
Radix is the established layer and what shadcn/ui builds on. Base UI is the newer effort from overlapping authors with a cleaner API. React Aria is the most thorough on internationalisation and screen reader behaviour, with the largest API surface of the three. Headless UI is the smallest and pairs naturally with Tailwind.
The choice matters less than committing to one: mixing two primitive libraries means two portal implementations and two focus models in one bundle.
The middle path most teams take
Start from shadcn/ui, which is primitives with a default visual layer, and replace that layer with your own tokens rather than starting from bare primitives. You get the behaviour, a working default while the design system is being built, and the ability to change anything, since the file is yours - the argument the customisation guide makes at four levels.
Going fully headless is worth it when the design system is genuinely ahead of the code, which is rarer than it sounds and unmistakable when it is true.
Where else to look
The honest list, because the answer is not always us:
| Source | Best for | Trade-off |
|---|---|---|
| Radix | The established primitive layer, widely used | No styling at all |
| Base UI | The same idea with a newer API | Younger ecosystem |
| React Aria | The most thorough internationalisation and screen reader work | The largest API surface |
| shadcn/ui | Primitives with a visual layer you own and can replace | An opinion you have to override |
| 21st | Components built on all of the above, with previews | Quality varies by author, so preview before you take it |
Taking one
Every component page has a live preview and the code. Installing goes through the shadcn CLI against our registry:
That key comes from your 21st account, and installs require a membership. Set API_KEY_21ST once in your shell and the command works for anything in the catalogue.
Frequently asked
- What do headless primitives actually give you?
- The expensive, invisible half: roving tabindex, typeahead, focus return, portals out of clipping ancestors, collision-aware positioning and correct ARIA relationships, tested across assistive technologies. Multiply that across a menu, dialog, combobox, select, tabs and tooltip and it is most of the work in a component library.
- When is going headless a mistake?
- When you do not have a design system yet, because every decision a styled library would have made now has to be made screen by screen; when the team is small, since maintaining a visual layer for twenty components is ongoing work; and when speed matters more than distinctiveness.
- Which headless library should you pick?
- Radix is the established layer and what shadcn builds on, Base UI is the newer effort with a cleaner API, React Aria is the most thorough on internationalisation and screen readers with the largest API, and Headless UI is the smallest. Committing to one matters more than which, since mixing two means two portal implementations.
- Is there a middle path?
- Yes, and most teams take it: start from shadcn/ui, which is primitives with a default visual layer, and replace that layer with your own tokens. You get the behaviour, a working default while the design system is built, and the ability to edit anything because the file is yours.



