EmpireUI
Get Pro
← Blog8 min read#shadcn#radix ui#comparison

shadcn/ui vs Radix UI: What's the Difference, Which Do You Need?

shadcn/ui is built on top of Radix UI — so which one do you actually install? Here's the real difference, and how to pick the right tool for your project.

Developer writing React code on a laptop with two browser windows open

Wait — They're Not Competitors

A lot of developers land in this debate thinking they need to pick one over the other. They don't. shadcn/ui *is* Radix UI, just with Tailwind CSS poured on top and the source code copied directly into your repo. That's the whole trick. Radix provides the headless, accessible component primitives; shadcn gives you opinionated, pre-styled wrappers around them that you own outright.

Radix UI (v1.0 dropped in 2022, now at v2.x) is a collection of unstyled React primitives — Dialog, DropdownMenu, Tooltip, Popover, and about 30 others. Every one handles keyboard navigation, ARIA roles, focus trapping, and screen reader announcements. You get zero visual styles. That's the point. You're supposed to bring your own.

shadcn/ui came along in 2023 and essentially said: 'What if we pre-styled all those Radix primitives with Tailwind utility classes, and instead of publishing them as an npm package, we just let you copy-paste the source into your own project?' That decision changed how a lot of teams think about component libraries. You don't install shadcn — you run npx shadcn-ui@latest add button and the Button component lands at ./components/ui/button.tsx, ready for you to hack on.

So the real question isn't 'shadcn vs Radix'. It's 'do I want raw headless primitives, or do I want pre-styled components I can modify freely?' The answer depends entirely on what you're building.

What Radix UI Actually Gives You

Radix is the accessibility layer. Full stop. If you've ever tried to build a keyboard-navigable dropdown from scratch — handling Escape to close, ArrowDown to traverse items, focus return on dismiss, correct ARIA role="menu" attributes — you know it's genuinely painful. Radix solves all of that so you never have to.

Installation is exactly what you'd expect from a conventional npm package. You pull in only the primitives you need: npm install @radix-ui/react-dialog brings Dialog, nothing else. This is great for bundle budgets. Each primitive is tree-shakeable, and most of them compress to well under 10kb gzipped.

In practice, raw Radix usage looks like this — every part of the component tree is composable via sub-components, and you style each one directly: ``tsx import * as Dialog from '@radix-ui/react-dialog'; export function MyModal() { return ( <Dialog.Root> <Dialog.Trigger asChild> <button className="your-button-classes">Open</button> </Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/50" /> <Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-xl w-96"> <Dialog.Title>Modal Title</Dialog.Title> <Dialog.Description>Some description text.</Dialog.Description> <Dialog.Close>Close</Dialog.Close> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ); } ` You're responsible for every className`. That's either freedom or a chore, depending on your team.

Worth noting: Radix also ships @radix-ui/colors, a perceptually balanced color system that pairs well with the primitives if you don't want to deal with color token decisions yourself. A lot of teams miss this and reach for shadcn just to get pre-made colors, when Radix already has the answer.

One more thing — Radix doesn't prescribe Tailwind. You can use CSS Modules, vanilla CSS, styled-components, or emotion. If your design system isn't Tailwind-based, raw Radix is almost certainly the right call.

What shadcn/ui Actually Gives You

shadcn/ui is opinionated in the best way. It took the Radix primitives, wrote solid Tailwind utility class compositions for each one, and handed you the source. No version lock. No API surface to navigate. Just files in your project that you control.

The component quality is genuinely high. The default <Button> ships with variant and size props using class-variance-authority (CVA), which is a clean pattern for component variants. The <Card>, <Badge>, <Skeleton> — these are all thoughtfully done, and they hit a visual bar that feels production-ready without being over-styled. Honestly, the defaults look better than most paid component libraries from 2020.

Here's what a typical shadcn component looks like after you add it: ``tsx // components/ui/button.tsx — this file lives in YOUR repo import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', ghost: 'hover:bg-accent hover:text-accent-foreground', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 rounded-md px-3', lg: 'h-11 rounded-md px-8', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'default' }, } ); export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {} export function Button({ className, variant, size, ...props }: ButtonProps) { return <button className={cn(buttonVariants({ variant, size, className }))} {...props} />; } ` You want to make the sm button 32px tall instead of 36px? Change h-9 to h-8`. Done. No forking, no overrides, no CSS specificity fights.

That said, shadcn has a clear constraint: it's Tailwind or bust. If your project doesn't use Tailwind, you'd be fighting the setup from minute one. And because you own the code, updating when shadcn releases improvements isn't automatic — you re-run the add command and resolve diffs manually.

Quick aside: the shadcn component registry also includes more opinionated, complex components like DataTable (built on TanStack Table), Calendar (built on react-day-picker), and Command (built on cmdk). These go beyond what raw Radix primitives cover, so shadcn isn't purely a styling layer — it also makes component composition decisions for you.

When to Use Each One (Real-World Breakdown)

Use raw Radix when your design system diverges from Tailwind, when you need maximum bundle control, or when you're building a white-label product where visual defaults would require fighting overrides constantly. Radix is also the better pick if you're maintaining a component library that other teams consume — you want to expose primitives, not bake in styling opinions.

Use shadcn when you're building a product UI fast and you want sensible defaults without committing to a locked component library. It's particularly strong on Next.js 14+ App Router projects — the setup wizard handles the cn utility, Tailwind config, and CSS variables automatically. If you've ever spent an afternoon bikeshedding a modal design, shadcn's defaults will save you from that decision entirely.

In practice, most mid-size product teams end up using both at the same time — shadcn for the majority of UI, plus raw Radix primitives for one-off components that don't exist in the shadcn registry. You might grab @radix-ui/react-toast standalone and style it yourself for a notification system that needs heavy custom animation. That's a completely legitimate pattern.

There's a third option worth knowing: if you need styles that go well beyond the neutral shadcn defaults — glassmorphism cards, aurora gradients, neobrutalism panels — a dedicated style library is often faster than customising shadcn. Empire UI's glassmorphism components or styles like neobrutalism give you these aesthetics pre-built, and they still compose with Radix primitives underneath. It's layers all the way down.

The Copy-Paste vs Package Debate

This is the part that divides people. shadcn's 'not an npm package' model felt weird in 2023. By 2026 it feels pretty normal — the tradeoff is clear. npm packages get updates automatically, but you're fighting the package API when you want to customise. Local source files never auto-update, but you have total control.

For most product teams, the update problem is manageable. shadcn components don't change that often once you've customised them. And when upstream does ship improvements — better accessibility on ComboBox, for example — you can selectively cherry-pick the diff. That's actually a better model than 'upgrade the package and hope your overrides still work'.

Radix primitives as npm packages, by contrast, are extremely stable. They maintain a strong semantic versioning commitment. The @radix-ui/react-dialog package you installed in 2023 still works today without changes. That predictability matters on large teams where you can't afford a surprise breaking change in a production deploy.

Look, neither model is universally better. The copy-paste model wins for speed and flexibility in product UI. The package model wins for stability and predictability in shared libraries. Know which problem you're solving, and you'll pick the right tool without agonising over it.

One thing that genuinely is a non-issue: performance. Both approaches result in essentially the same runtime JavaScript. Radix primitives are already in shadcn components. The abstraction doesn't add meaningful overhead. You can also pair either with Empire UI's gradient generator or box shadow generator to produce custom CSS values that drop cleanly into your class strings.

Setting Up Each One in 2026

Raw Radix is a standard npm install — just be specific about which primitives you want: ``bash # Install only what you need npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-tooltip `` No config files. No CLI. Just import and use. The peer dependency is React 16.8+ (hooks), so you're almost certainly covered.

shadcn has a proper setup wizard as of v0.8.0: ``bash # In a Next.js or Vite project with Tailwind already configured npx shadcn-ui@latest init # Then add components one at a time npx shadcn-ui@latest add button dialog dropdown-menu ` The init command writes a components.json config, patches your tailwind.config.ts with the correct content paths and CSS variables, and creates lib/utils.ts with the cn` helper. It's genuinely painless on a fresh project. On an existing project with non-standard folder structure, you may need to adjust the config manually.

One common friction point: shadcn generates components in components/ui/ by default. If your project structure is different — say, src/design-system/ — update components.json before running any add commands. Fixing that after you've added 20 components is annoying at 16 components deep.

That said, both libraries work fine with or without TypeScript. shadcn's generated output is TypeScript by default (.tsx), but you can configure it to emit plain .jsx if your project hasn't migrated yet. Radix primitives ship their own .d.ts type definitions, so TypeScript support is first-class regardless.

FAQ

Is shadcn/ui just a wrapper around Radix UI?

Mostly yes. shadcn uses Radix primitives for interactive components like Dialog, DropdownMenu, and Tooltip, then layers Tailwind utility classes on top. Some shadcn components (like DataTable) use entirely different libraries such as TanStack Table.

Do I need to install Radix UI separately if I'm using shadcn?

No — when you add a shadcn component that uses Radix, the CLI automatically adds the correct @radix-ui/* package to your dependencies. You don't manage Radix installs manually.

Can I use Radix UI without Tailwind?

Yes, and that's one of its strengths. Radix is completely unstyled, so you can use CSS Modules, styled-components, emotion, or plain CSS. It has no opinion about your styling approach.

How do I update shadcn components when a new version is released?

Re-run npx shadcn-ui@latest add <component> and resolve the diff against your customised version. There's no automatic update — that's intentional, since you own the source.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

shadcn/ui Alternatives: What to Use When shadcn Isn't EnoughHeadless UI Libraries in 2026: Radix, Headless UI, Ark ComparedReact Modal / Dialog: Headless UI, Radix UI and the Vanilla WayTailwind vs CSS Modules in 2026: Which One Should You Actually Use?