/

Headless Primitives: When Unstyled Is the Right Answer

You are buying the part that takes longest and is least visible. Whether that trade is good depends on whether you already have a design.

Serafim Korablev
Serafim Korablev
@korablev

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:

SourceBest forTrade-off
RadixThe established primitive layer, widely usedNo styling at all
Base UIThe same idea with a newer APIYounger ecosystem
React AriaThe most thorough internationalisation and screen reader workThe largest API surface
shadcn/uiPrimitives with a visual layer you own and can replaceAn opinion you have to override
21stComponents built on all of the above, with previewsQuality 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:

bash

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.

Browse components →

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.

Published

Aug 21, 2026

Read time

4 min

Tags

ComparisonPrimitivesReactAccessibility

Share