Skip to main content

shadcn/ui vs Radix UI: Component Library vs 2026

·PkgPulse Team
0

TL;DR

shadcn/ui and Radix UI solve different layers of the same problem. Radix UI provides accessible, unstyled primitives (a dependency you install). shadcn/ui uses those same Radix primitives but copies the components directly into your project — giving you full control over every line. For most production apps, shadcn/ui is the right starting point: you own the code, you can customize without fighting a library's API, and Tailwind integration is seamless. Use Radix UI directly when you want the raw primitives without shadcn's Tailwind opinions.

Key Takeaways

  • shadcn/ui is not a component library — it's a code generator that copies components into your project
  • Both use Radix UI under the hood — shadcn adds Tailwind styling on top
  • shadcn/ui: you own the code, full customization, no library version conflicts
  • Radix UI: install once, get updates via npm, bring your own CSS
  • Bundle size: only pay for what you install (npx shadcn-ui add button = one file)

The Key Conceptual Difference

Traditional component library (e.g., MUI, Chakra, Mantine):
  node_modules/
    @mui/material/      ← library lives here, you don't touch it
      Button.tsx        ← you can't easily change the internals
      Dialog.tsx        ← CSS overrides fight the library
      ...
  Your code imports and uses it. You control nothing inside.

Radix UI:
  node_modules/
    @radix-ui/react-dialog/   ← primitive, completely unstyled
    @radix-ui/react-dropdown-menu/
    ...
  You get accessibility + behavior. Bring your own CSS.
  Still a dependency — updates come via npm.

shadcn/ui:
  src/components/ui/
    button.tsx      ← YOUR file. In your repo. You own it.
    dialog.tsx      ← Copy-pasted from shadcn, now yours to modify
    dropdown-menu.tsx
  node_modules/@radix-ui/... ← still used underneath

  The shadcn CLI copies component code into your project.
  It's not a dependency. There's nothing to "upgrade."
  You own every line of every component.

Installing and Using shadcn/ui

# Initialize shadcn/ui in an existing project:
npx shadcn-ui@latest init
# Prompts: style (Default/New York), base color, CSS variables (yes/no)
# Creates: components.json, adds Tailwind config updates, adds cn() utility

# Add individual components (copies file to src/components/ui/):
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add dropdown-menu
npx shadcn-ui@latest add form  # includes react-hook-form integration
npx shadcn-ui@latest add table
npx shadcn-ui@latest add toast

# What "add button" does:
# 1. Creates src/components/ui/button.tsx in YOUR project
# 2. Installs @radix-ui/react-slot (if not present)
# 3. That's it — the button code is now yours

# The resulting button.tsx:
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 rounded-md text-sm font-medium...",
  {
    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",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      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" },
  }
)

// You can add a new variant by editing THIS file:
// variant: { brand: "bg-brand-500 text-white hover:bg-brand-600" }
// No library update needed. No PR to the library. Just edit the file.

Radix UI: Primitives Without Opinions

// Using Radix UI directly (without shadcn):
import * as Dialog from '@radix-ui/react-dialog';

// Completely unstyled — you bring all the CSS:
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <button className="your-button-styles">Delete</button>
      </Dialog.Trigger>

      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50 animate-overlay-show" />
        <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 w-full max-w-md">
          <Dialog.Title className="text-xl font-bold">Confirm Delete</Dialog.Title>
          <Dialog.Description className="text-gray-600 mt-2">
            This action cannot be undone.
          </Dialog.Description>
          <div className="flex gap-3 mt-6 justify-end">
            <Dialog.Close asChild>
              <button className="your-cancel-button">Cancel</button>
            </Dialog.Close>
            <button onClick={onConfirm} className="your-danger-button">
              Delete
            </button>
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

// What Radix handles for you (accessibility):
// → Focus trapping within the dialog
// → Escape key closes dialog
// → Screen reader announcements (role="dialog", aria-modal)
// → Scroll lock on body when open
// → Portal rendering outside the DOM hierarchy

// What you provide:
// → All CSS/styling
// → Animation (or use Radix's data-state attributes for CSS animations)
// → Layout and spacing

shadcn/ui vs Radix UI: Direct Comparison

Feature                    shadcn/ui              Radix UI
─────────────────────────────────────────────────────────
Styling                    Tailwind (CVA)         None (unstyled)
Installation               File copy (npx add)    npm install
Code ownership             Yours to edit          Library (read-only)
Updates                    Re-run add command     npm update
Customization              Edit the file          CSS + className
Design system              Default + New York     None
Dark mode                  Built-in (CSS vars)    Roll your own
Accessibility              Inherited from Radix   Built-in
TypeScript                 Full types             Full types
Component count            50+ pre-built          30+ primitives
Bundle impact              Only what you add      Only what you install
Dependency conflicts       None (it's your code)  Possible with major updates
Learning curve             Low (Tailwind knowledge)  Moderate (compose parts)

Real Customization: Why Code Ownership Matters

// Real scenario: you need a Button with a loading spinner
// shadcn/ui (you own the code):
// Edit src/components/ui/button.tsx directly:

const buttonVariants = cva("...", {
  variants: {
    variant: {
      // ... existing variants
    },
    size: {
      // ... existing sizes
    },
    // ADD THIS — no library PR needed:
    loading: {
      true: "opacity-70 pointer-events-none",
    },
  },
});

export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>,
  VariantProps<typeof buttonVariants> {
  asChild?: boolean;
  loading?: boolean;  // ADD THIS
  loadingText?: string;  // ADD THIS
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ loading, loadingText, children, ...props }, ref) => {
    return (
      <button ref={ref} disabled={loading || props.disabled} {...props}>
        {loading && <Spinner className="mr-2 h-4 w-4 animate-spin" />}
        {loading ? (loadingText ?? children) : children}
      </button>
    );
  }
);

// In a traditional library, you'd:
// → File a GitHub issue
// → Wait for it to be accepted
// → Wait for the release
// → Update the dependency
// → Deal with any other breaking changes in that release
// OR: wrap the component in your own, fighting the library's styles

// Total time: weeks to months
// shadcn/ui approach: 15 minutes, ships today

When to Use What

Use shadcn/ui when:
→ You're on Tailwind CSS already (it's a natural fit)
→ You want to ship fast with polished components
→ You need to customize components beyond what a library API allows
→ You want zero version conflict risk on UI components
→ Building a product with a custom design system
→ The "New York" or "Default" style matches your aesthetic

Use Radix UI directly (without shadcn) when:
→ You're NOT on Tailwind (CSS Modules, CSS-in-JS, vanilla CSS)
→ You have an existing design system and just need accessible primitives
→ You want updates to flow in via npm automatically
→ You're building a component library of your own to distribute
→ You want maximum control over the HTML structure

Use neither (use Mantine, Chakra, or MUI) when:
→ You need a complete design system with theme tokens out of the box
→ Your team doesn't use Tailwind and doesn't want to manage component files
→ You need enterprise support guarantees
→ You need date pickers, complex data grids, or charts included

The 2026 recommendation:
→ For most new React projects: shadcn/ui
→ It's won the "React component library" debate for SPAs
→ GitHub stars: 65K+ and growing
→ The developer experience of owning your components beats fighting a library
→ When you need primitives for a custom system: Radix UI directly

The Model Shift: Distribution vs Ownership

The component library landscape before shadcn/ui had a fundamental tension: libraries distributed via npm are easy to install but hard to customize beyond their API surfaces. Teams that needed deeply custom UI — a button with a loading state, a dialog with custom focus management, a dropdown with a search input — faced a choice between forking the library (losing updates), wrapping it (fighting CSS specificity), or accepting limitations.

shadcn/ui resolved this by decoupling distribution from installation. The components are distributed via a CLI that generates source code rather than via npm that installs a compiled package. This shifts ownership completely: once a component is copied into your project, it belongs to your codebase. You apply updates by re-running the generator command for the component, which overwrites the file — a deliberate choice that keeps you thinking about the component as code you own rather than a dependency you consume. This ownership model is unconventional but aligns well with how experienced teams already handled component libraries: they typically ended up wrapping or modifying them anyway.

Why shadcn/ui Spread So Fast

shadcn/ui reached 65,000 GitHub stars faster than almost any developer tool in recent memory, and understanding why helps clarify what it is actually solving. Before shadcn/ui, React developers faced a frustrating choice: use a full component library like MUI or Chakra and accept its aesthetic and API constraints, or build every component from scratch with full control but significant effort. There was no practical middle ground that combined pre-built, accessible components with complete ownership of the code.

shadcn/ui filled this gap by reframing the problem. Instead of shipping components as a dependency, it ships them as a CLI that copies component source code into your project. The components are built on Radix UI primitives (which handle accessibility correctly) and styled with Tailwind CSS (which most modern React projects already use). The combination means you get components that are accessible by default, styled in a system you already understand, and fully owned by your codebase. The viral spread happened because developers tried it, found it matched their existing workflow perfectly, and told their colleagues.

The Accessibility Inheritance Chain

One of shadcn/ui's strongest technical arguments is that it inherits Radix UI's accessibility work rather than duplicating or reimplementing it. Radix UI was built specifically to implement ARIA patterns correctly — dialog, dropdown menu, popover, tooltip, and other interactive patterns all require specific keyboard navigation, focus management, and screen reader behavior that is easy to get wrong. The WAI-ARIA specification for a modal dialog alone requires focus trapping, escape key handling, role="dialog" with aria-modal, and proper return-focus behavior on close.

Radix UI implements all of these correctly and has been tested against screen readers across platforms. When shadcn/ui wraps a Radix Dialog primitive, it inherits all that accessibility behavior automatically. This inheritance chain means that a team using shadcn/ui gets enterprise-grade accessibility without accessibility expertise in-house. The risk is that if you modify a shadcn/ui component in ways that break its Radix structure — wrapping the content outside the Dialog.Content, removing Portal rendering, or replacing Dialog.Close with a plain button — you can break the accessibility behavior that Radix provides. Understanding the Radix structure underneath your shadcn components is important before making structural modifications.

Updating shadcn/ui Components

One of the most common concerns about shadcn/ui's code-copy model is the update story. If Radix UI fixes a bug or adds a new feature, how do you get that fix into your copied component code? The answer is: npx shadcn-ui@latest add button --overwrite. This re-copies the component from the current shadcn/ui source, overwriting your local version.

This works cleanly if you have not modified the component. If you have — adding a loading prop, changing the variant colors, or extending the TypeScript interface — you need to manually reconcile the update with your changes. In practice, most teams find that their component customizations live in one of two places: either in the component file itself (where they conflict with updates) or in CSS variables (where they don't conflict, since shadcn uses CSS custom properties for theming). The recommended pattern is to make visual customizations via CSS variables in your global stylesheet and reserve file modifications for structural or behavioral changes. This minimizes the conflict surface when updates arrive.

Building Design Systems with shadcn as a Foundation

Many teams use shadcn/ui not as a finished design system but as a starting point for building their own. The code-copy model supports this perfectly: you initialize shadcn, add the components you need, then systematically customize each component to match your brand. The result is a design system where every component is in your repository, every design decision is visible in code, and the team can understand and modify any component without consulting external documentation.

This "fork and own" approach is different from traditional design system strategies, which typically involve publishing a private npm package of custom components. The npm package approach has advantages — version control across multiple projects, ability to publish updates to all consumers — but it also requires build infrastructure for the package, publishing workflows, and version management. For organizations with a single product or a small number of tightly coupled products, the shadcn approach of copying components directly into each repository is often simpler and faster to iterate on.

Radix UI's Role in the Ecosystem Beyond shadcn

It is easy to think of Radix UI purely as the library that powers shadcn/ui, but Radix has an independent role in the ecosystem as the foundation for multiple component libraries. Mantine, Park UI, Chakra UI v3 (via Ark UI, which is built by the same team), and several smaller libraries all use Radix primitives for their accessible behavior layer. This ecosystem concentration reflects the quality of Radix's primitive implementations — the accessibility work is genuinely hard, and having it abstracted into a well-maintained library that multiple component libraries can share is an ecosystem efficiency gain.

Using Radix directly (without shadcn) makes most sense when you are building a component library that you intend to share across projects or publish as a package. In that case, you want to control the styling API — possibly CSS modules, CSS-in-JS, or vanilla-extract rather than Tailwind — and the shadcn component files (which are Tailwind-specific) are less useful than raw Radix primitives. The distinction is: use shadcn/ui when you are building a product application; use Radix directly when you are building a component system to distribute.

Compare shadcn/ui, Radix UI, and other React component library download trends at PkgPulse.

Compare Shadcn and Radix package health on PkgPulse.

See also: React vs Vue and React vs Svelte, Best React Component 2026: shadcn, Mantine, Chakra.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.