shadcn/ui vs Radix UI vs Headless UI
TL;DR
shadcn/ui is the default in 2026 for good reason: accessible components you own completely, built on Radix UI primitives, styled with Tailwind, and incrementally adoptable. Radix UI is what shadcn/ui is built on — use it if you want to build your own design system from primitives. Headless UI is Tailwind Labs' offering, smaller component selection, less maintained than the alternatives.
The Landscape
| Library | Model | Styled? | Accessible | Components | Bundle |
|---|---|---|---|---|---|
| shadcn/ui | Copy-paste | ✅ Tailwind | ✅ | 50+ | You own it |
| Radix UI | npm package | ❌ Unstyled | ✅ | 35+ | ~40KB |
| Headless UI | npm package | ❌ Unstyled | ✅ | 10 | ~20KB |
| Chakra UI | npm package | ✅ | ✅ | 60+ | ~100KB |
| MUI | npm package | ✅ | ✅ | 100+ | ~300KB |
shadcn/ui: The Copy-Paste Revolution
shadcn/ui isn't a component library in the traditional sense — you don't install it as a dependency. You copy the components into your project.
# Install CLI
npx shadcn@latest init
# Add individual components — copies source code to your project
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add data-table
npx shadcn@latest add command # Command palette (cmdk)
The components land in components/ui/ as source files you fully own and modify.
Why This Model Wins
// components/ui/button.tsx — it's yours, edit freely
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
Need a loading variant? Add it. Need different sizes? Edit the CVA config. No fighting with library internals, no !important overrides, no waiting for a PR to merge.
shadcn/ui Theming
shadcn/ui uses CSS variables for theming — swap the entire design system by changing variables:
/* globals.css — light theme */
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
/* ... */
}
/* Dark mode */
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
/* ... */
}
Change --primary to your brand color and every button, badge, and ring updates.
Radix UI: The Primitives Layer
Radix UI provides unstyled, accessible component primitives. shadcn/ui is built on top of Radix — when you use shadcn, you're using Radix underneath.
// Using Radix UI directly — maximum control, no styles
import * as Dialog from '@radix-ui/react-dialog';
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="my-trigger-styles">Open</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="my-overlay-styles" />
<Dialog.Content className="my-content-styles">
<Dialog.Title>Dialog Title</Dialog.Title>
<Dialog.Description>Dialog content here</Dialog.Description>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
When to Use Radix UI Directly
- Building a custom design system that doesn't use Tailwind
- White-label product where the styling must be fully custom
- Large team with a dedicated design system engineer
- Using CSS-in-JS (Emotion, styled-components) instead of Tailwind
For most SaaS projects, shadcn/ui wraps Radix perfectly — use Radix directly only if shadcn's Tailwind styling doesn't fit your constraints.
Headless UI: Tailwind Labs' Component Library
Headless UI is maintained by the Tailwind CSS team. Fewer components than Radix, but well-integrated with Tailwind.
import { Dialog, Transition } from '@headlessui/react';
import { Fragment } from 'react';
function MyDialog({ isOpen, onClose }) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={onClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<div className="fixed inset-0 bg-black/25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<Dialog.Panel className="bg-white rounded-xl p-6">
<Dialog.Title>My Dialog</Dialog.Title>
{/* content */}
</Dialog.Panel>
</div>
</Dialog>
</Transition>
);
}
Headless UI in 2026
Headless UI has fallen behind Radix UI in component coverage and maintenance velocity. The component count (10 vs Radix's 35+) limits it for full SaaS builds. Tailwind UI (the paid design kit) uses it — if you've purchased Tailwind UI, you're already using Headless UI and it integrates perfectly.
Choose Headless UI if: You've purchased Tailwind UI and want components consistent with the design kit.
Boilerplate Defaults
| Boilerplate | Component Library |
|---|---|
| ShipFast | shadcn/ui |
| Supastarter | shadcn/ui |
| Makerkit | shadcn/ui |
| T3 Stack | shadcn/ui (community) |
| Epic Stack | Radix UI (custom) |
| Open SaaS | shadcn/ui |
Recommendation
For a new SaaS in 2026:
- shadcn/ui — default choice. Best DX, most maintained, boilerplate ecosystem support.
- Radix UI directly — only if you're building a fully custom design system.
- Headless UI — only if using Tailwind UI's design kit.
- Chakra/MUI — consider if switching from React-ecosystem-lock-in is important, or if your team already knows them well.
Find boilerplates by UI library on StarterPick.
Check out this boilerplate
View shadcn/ui on StarterPick →