shadcn/ui vs Radix UI primitives: when to use each
shadcn/ui is built on Radix — that changes the question. Use shadcn on Tailwind teams; go straight to Radix for design systems needing strict brand control.
By Ethan · Updated May 17, 2026
1,358 words · 7 min read
Use shadcn/ui if you are on Tailwind and want working, accessible components in under an hour. Go straight to Radix primitives if you are building a design system with strict brand control, or if your team is on CSS-in-JS. Those are different jobs. The confusion usually comes from not knowing they are the same code underneath.
Who this is for
React developers who have seen both names, aren’t sure of the relationship, and need a direct answer before picking one. This covers [email protected] (released 2026-05-05) and [email protected] (unified package, released 2025-08-13). Both are React-only — if you are still evaluating front-end frameworks, our React vs Vue guide covers that decision first.
What each one is
shadcn/ui
The documentation says it plainly: “This is not a component library. It is how you build your component library.”
When you run npx shadcn add dialog, you are not installing a package. You are copying a .tsx file into your repo. That file lives in your codebase, you own it, you edit it directly. There is no upstream component version to upgrade.
As of May 2026, shadcn/ui has 114,504 GitHub stars and supports Next.js, Vite, TanStack Start, React Router, Astro, and Laravel.
Radix UI primitives
Radix ships unstyled, accessible components as npm packages. You install @radix-ui/react-dialog, compose the sub-components, and bring your own CSS. Nothing is styled. Nothing is decided for you.
import { Popover } from "radix-ui";
<Popover.Root>
<Popover.Trigger>More info</Popover.Trigger>
<Popover.Portal>
<Popover.Content>
Some more info…
<Popover.Arrow />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
That is a complete, accessible popover. The styling is entirely your problem.
The relationship between them
shadcn/ui is a layer on top of Radix. The stack is:
Your app code
↓
shadcn/ui components (source in your repo)
↓
Radix UI primitives (installed as npm deps: @radix-ui/react-*)
↓
Tailwind CSS v4 (styling via CSS variables)
When you add a shadcn Dialog, it installs @radix-ui/react-dialog from npm and adds a wrapper component to your source tree. The Radix package does the accessibility work — keyboard trapping, ARIA attributes, focus management. shadcn adds the visual layer and the opinionated Tailwind classes.
This matters for bundle size: shadcn adds zero runtime overhead beyond Radix itself. Both approaches ship the same Radix packages. The difference is a few hundred bytes of copied .tsx and Tailwind class strings.
Customization ceiling
How shadcn handles it
shadcn has two layers. The first is a CSS variable token system — semantic tokens like --primary, --background, --radius that components reference. Override them in your global CSS, and the whole UI shifts. It uses oklch() in Tailwind v4, which gives you a wider gamut to work with.
The second layer is the source code itself. Because the component lives in your repo, there is no API to work around. You want a different hover state on the button? Edit the button. No prop drilling. No theme override DSL to learn.
The tradeoff: you are on Tailwind whether you want to be or not. If you are still deciding between Tailwind and a scoped-CSS approach, our Tailwind vs CSS Modules comparison covers the long-term tradeoffs.
How Radix handles it
Radix is fully unstyled by design. You attach classes from whatever system you use — CSS Modules, styled-components, Emotion, vanilla CSS, Tailwind. The asChild prop lets you render your own element instead of Radix’s default:
<Button asChild>
<a href="/login">Log in</a>
</Button>
That composability is why teams building real design systems prefer Radix. You get the behavior (focus, ARIA, keyboard) without any visual defaults to undo.
Bundle impact
Numbers from Bundlephobia, retrieved 2026-05-17 (all packages are tree-shakeable, ESM entry point):
| Component | Radix package | Gzipped |
|---|---|---|
| Dialog | @radix-ui/react-dialog 1.1.15 | 10.6 kB |
| Tabs | @radix-ui/react-tabs 1.1.13 | 4.9 kB |
| Select | @radix-ui/react-select 2.2.6 | 23.7 kB |
| Dropdown Menu | @radix-ui/react-dropdown-menu 2.1.16 | 24.3 kB |
The larger sizes on Select and Dropdown are driven by Floating UI (the positioning engine), which adds about 38 kB minified to those packages.
Using shadcn changes none of these numbers. You install the same Radix packages either way. The weekly download count for @radix-ui/react-dialog — 51M per week — reflects this: most of those installs are shadcn projects pulling Radix as a transitive dependency.
When to pick shadcn/ui
- Your team already uses Tailwind — there is no friction.
- You need working UI fast and do not have a designer who will produce a full token system.
- You want code ownership: shadcn components in your repo can have Radix bugs patched directly, without waiting for an upstream release.
- You want the community surface: a large body of extensions, examples, and Stack Overflow answers.
When to use Radix primitives directly
- You are building a proprietary design system with strict brand control. Vercel and Linear both reached for raw Radix, not shadcn. Vercel needed to own every visual token. Linear built their Orbiter design system on Radix with styled-components.
- Your stack is CSS-in-JS or CSS Modules — Tailwind coupling would require a parallel migration.
- You are building white-label or multi-tenant UI where no aesthetic defaults can ship.
- You want precise per-primitive bundle auditing.
Vercel’s engineering team put it directly: “We’ve been able to focus on building solid user experiences on top of Radix Primitives.” Their fragmented stack (custom components + Reach UI + React Spectrum) consolidated onto Radix because they needed a consistent, accessible foundation their design team could style from scratch.
The Radix maintenance situation
This is context you should have before committing to either path.
Radix’s maintenance cadence has slowed since the WorkOS acquisition. Long-lived bugs remain open for a year or more. A GitHub Discussion on the shadcn repo — 55 reactions — calls for migrating from Radix to MUI’s Base UI. One commenter: “Every two weeks or so I come across a bug in radix-ui that’s been opened years ago.”
The concrete pain point: HoverCard instances at scale (50–100) can trigger “Maximum update depth exceeded” due to a missing useEffect dependency in @radix-ui/react-popper. This class of internal setState storm appears across multiple open issues.
shadcn’s answer: as of late 2025, new projects can opt into Base UI (MUI’s headless primitive library) as the underlying engine instead of Radix, while keeping the same component API. If you are starting a new shadcn project today and have concerns about Radix’s trajectory, that option exists.
If you are using Radix primitives directly, watch the issue tracker. For most components (Tabs, Accordion, Dialog without extreme scale) the library works fine. For anything involving floating positioning at scale, test your specific workload.
Verdict
Pick shadcn/ui if you are a Tailwind team that wants accessible components without building a design system from scratch. The defaults are good, the customization model is direct, and Base UI support gives you an exit ramp from Radix if you need it.
Pick Radix primitives directly if you are building a proprietary design system, if Tailwind coupling is a blocker, or if you need white-label control over every visual token. Expect to do the styling work yourself — that is the point.
The projects that get confused are the ones that reach for Radix primitives thinking they get styled components, or reach for shadcn thinking they can detach from Tailwind. You can’t. Both choices are good; they are just good at different things.
Caveats
No affiliate links in this article — no Vercel or component library affiliate program was available at the time of writing. All bundle numbers are from Bundlephobia’s public API as of 2026-05-17. The shadcn + Base UI integration was in active development in late 2025; consult the current docs before adopting it in production.
References
- shadcn/ui documentation
- shadcn/ui theming
- shadcn/ui GitHub
- Radix UI introduction
- Radix UI getting started
- Vercel case study — raw Radix
- Linear case study — raw Radix
- shadcn vs Radix community discussion
- Base UI migration discussion (74 votes)
- HoverCard scale bug
- Radix future risk analysis
- WorkOS: shadcn vs Radix explainer