Best React Component Libraries in 2026: shadcn vs Radix vs Headless UI
·PkgPulse Team
TL;DR
shadcn/ui for copy-paste components with Tailwind; Radix UI for unstyled accessible primitives; Headless UI for Tailwind-first. shadcn/ui (~1.5M weekly downloads) isn't a traditional library — you copy components into your project and own the code. Radix UI (~4M) provides unstyled, WAI-ARIA-compliant primitives. Headless UI (~2M) is Tailwind Labs' headless library. For most new React projects in 2026, shadcn/ui is the starting point.
Key Takeaways
- Radix UI: ~4M weekly downloads — unstyled primitives, shadcn/ui is built on top of it
- Headless UI: ~2M downloads — Tailwind-first headless components, Tailwind Labs
- shadcn/ui: ~1.5M downloads — copy-paste, owns the code, Radix + Tailwind
- shadcn/ui — not installed as a dependency; CLI adds component files to your project
- 2026 trend — headless + styling (shadcn pattern) dominates over pre-styled libraries
shadcn/ui (Copy-Paste)
# shadcn/ui — setup (Next.js)
npx shadcn@latest init
# Choose: TypeScript, Tailwind, CSS variables, color scheme
# Add components
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add form
npx shadcn@latest add table
npx shadcn@latest add toast
// shadcn/ui — Dialog (you own this code in components/ui/dialog.tsx)
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export function EditUserDialog({ user, onSave }) {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit User</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Edit User</DialogTitle>
<DialogDescription>
Make changes to the user profile here.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right">Name</Label>
<Input id="name" defaultValue={user.name} className="col-span-3" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="email" className="text-right">Email</Label>
<Input id="email" defaultValue={user.email} className="col-span-3" />
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={() => onSave()}>Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
// shadcn/ui — Form with React Hook Form + Zod (built-in integration)
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
const formSchema = z.object({
email: z.string().email(),
name: z.string().min(2, 'Name must be at least 2 characters'),
});
export function UserForm({ onSubmit }) {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: '', name: '' },
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage /> {/* Shows Zod validation errors */}
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Radix UI (Headless Primitives)
// Radix UI — build your own styled component on top of primitives
import * as Dialog from '@radix-ui/react-dialog';
import * as Select from '@radix-ui/react-select';
// Custom styled dialog (bring your own CSS)
function MyDialog({ children, title }) {
return (
<Dialog.Root>
<Dialog.Trigger className="btn-primary">
Open
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 shadow-xl max-w-md w-full">
<Dialog.Title className="text-xl font-bold mb-4">{title}</Dialog.Title>
{children}
<Dialog.Close className="absolute top-4 right-4 text-gray-500 hover:text-gray-900">
✕
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// Custom styled select (WAI-ARIA compliant automatically)
function MySelect({ options, onChange }) {
return (
<Select.Root onValueChange={onChange}>
<Select.Trigger className="flex items-center gap-2 px-3 py-2 border rounded">
<Select.Value placeholder="Select option..." />
<Select.Icon>▾</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="bg-white border rounded-lg shadow-lg">
<Select.Viewport>
{options.map(opt => (
<Select.Item key={opt.value} value={opt.value} className="px-3 py-2 hover:bg-gray-100 cursor-pointer">
<Select.ItemText>{opt.label}</Select.ItemText>
</Select.Item>
))}
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
);
}
Headless UI (Tailwind-Native)
// Headless UI — Tailwind-first, from Tailwind Labs
import { Dialog, Transition, Listbox } from '@headlessui/react';
import { Fragment, useState } from 'react';
function MyDialog({ isOpen, onClose, title, children }) {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog onClose={onClose} className="relative z-50">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
>
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
>
<Dialog.Panel className="bg-white rounded-xl p-6 shadow-xl max-w-md w-full">
<Dialog.Title className="text-xl font-bold">{title}</Dialog.Title>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
);
}
When to Choose
| Scenario | Pick |
|---|---|
| New project, want great default styles | shadcn/ui |
| Build your own design system | Radix UI |
| Tailwind-first, minimal setup | Headless UI |
| Already have design tokens/brand | Radix UI |
| Want full component ownership | shadcn/ui |
| Heavy customization needed | Radix UI (most flexible) |
| Enterprise design system | MUI or Ant Design |
Compare component library package health on PkgPulse.
See the live comparison
View shadcn vs. radix on PkgPulse →