The React component library debate used to be straightforward: pick an npm package, install it, use it. Then shadcn/ui arrived and proposed something radical: don't install components - copy them into your codebase.
Two years later, both approaches have proven themselves in production. This article is an honest comparison of the copy-paste model (shadcn/ui) and the npm package model (Ninna UI) - examining the trade-offs that matter at scale.
The Two Models
shadcn/ui: Copy-Paste
You run a CLI command that copies React component source files directly into your project. The components use Radix UI primitives and Tailwind CSS for styling. You own every line of code.
npx shadcn@latest add button dialog select
This creates files in your components/ui/ directory. They're yours. Modify them freely.
Ninna UI: npm Packages
You install packages from npm. Components are consumed as imports. You customize via CSS custom properties, Tailwind utilities, and data-slot CSS selectors - without modifying source code.
pnpm add @ninna-ui/primitives @ninna-ui/overlays @ninna-ui/forms
import { Button } from "@ninna-ui/primitives";
import { Modal } from "@ninna-ui/overlays";
import { Select } from "@ninna-ui/forms";
The Trade-Offs
1. Updates and Maintenance
shadcn/ui:
When a bug is fixed upstream, you have two options:
- Re-copy the component (losing your customizations)
- Manually apply the patch to your local copy
There's no npm update. No changelog. No semantic versioning. Each project has a unique fork that diverges from upstream over time.
Ninna UI:
Standard npm workflow. pnpm update @ninna-ui/primitives gets you bug fixes, accessibility improvements, and new features - while preserving your customizations (which live in CSS, not in component source).
pnpm update @ninna-ui/primitives
# Changelog at https://ninna-ui.dev/changelog
Verdict: npm packages win for teams that value maintenance predictability. Copy-paste wins for developers who rarely need upstream updates.
2. Customization Depth
shadcn/ui:
You own the code. You can change anything - the component structure, the HTML output, the ARIA behavior, the prop API. There's no abstraction between you and the implementation.
// You can literally rewrite the component
export function Button({ className, ...props }) {
return (
<button
className={cn("my-completely-custom-button-styles", className)}
{...props}
/>
);
}
Ninna UI:
You customize via 3 layers without touching source code:
-
Tailwind utilities -
classNameprop on every component -
CSS custom properties - theme tokens (
--color-primary,--radius-lg) - data-slot selectors - target any internal element
/* Customize any internal element via CSS */
[data-slot="trigger"] {
border-radius: var(--radius-full);
font-weight: 600;
}
[data-slot="content"] {
animation: slide-down 200ms ease-out;
}
98 data-slot targets cover every customizable element across all 69 components - without forking a single file.
Verdict: shadcn/ui wins for unlimited structural changes. Ninna UI wins for styling customization without the maintenance cost of owning source code.
3. Cross-Project Consistency
shadcn/ui:
Each project gets a unique copy of every component. Over time, these diverge. A Button in Project A is different from Button in Project B. Keeping them synchronized requires manual discipline or a private component library (which is... an npm package).
Ninna UI:
Every project uses the same @ninna-ui/primitives version. Same API, same accessibility, same behavior. Different visual styles via CSS theme presets - but the component contracts are identical.
/* Project A: Default theme */
@import "@ninna-ui/core/theme/presets/default.css";
/* Project B: Ocean theme */
@import "@ninna-ui/core/theme/presets/ocean.css";
/* Same components, different look */
Verdict: npm packages win decisively for multi-project teams and agencies.
4. Dependency Management
shadcn/ui:
Each copied component may depend on specific Radix UI packages. A typical shadcn/ui project accumulates 10-15+ Radix packages:
{
"@radix-ui/react-dialog": "^1.x",
"@radix-ui/react-dropdown-menu": "^2.x",
"@radix-ui/react-select": "^2.x",
"@radix-ui/react-tooltip": "^1.x",
"@radix-ui/react-popover": "^1.x",
"@radix-ui/react-accordion": "^1.x",
"class-variance-authority": "^0.7",
"cmdk": "^1.x",
"lucide-react": "^0.x"
// ... more for each component
}
You manage version compatibility yourself. When Radix releases a breaking change, you update each component individually.
Ninna UI:
Radix is an internal implementation detail. You never import it. You never manage its versions. Ninna UI wraps 11 Radix primitives behind clean interfaces:
{
"@ninna-ui/primitives": "^0.4.1",
"@ninna-ui/overlays": "^0.4.1",
"@ninna-ui/forms": "^0.4.1"
}
Three dependencies instead of fifteen. Radix version management is Ninna UI's responsibility, not yours.
Verdict: npm packages win for dependency hygiene. Copy-paste forces you to be a Radix version manager.
5. Bundle Size
shadcn/ui:
You only include the components you use, and since they're local files, tree-shaking is perfect. No unused code reaches the bundle.
However, each component pulls its own Radix dependency, and multiple components may include overlapping utility code (cn(), cva(), etc.).
Ninna UI:
12 tree-shakeable ESM packages with sideEffects: false. Import only what you use - unused components are eliminated by your bundler.
The theming layer is pure CSS (zero JavaScript), which means your theming overhead is literally 0 KB of JavaScript. shadcn/ui's theming is also CSS-based, so this is roughly equivalent.
Verdict: Roughly equal. Both approaches tree-shake well. Ninna UI has a slight edge because Radix dependencies are deduplicated internally.
6. TypeScript Experience
shadcn/ui:
Types depend on how well you maintain the copied code. Out of the box, components use cva for variant typing, which provides basic prop safety. But types can degrade as you modify components.
Ninna UI:
Full TypeScript generics on every component, maintained by the library team. Generic Select<T>, discriminated union props, strict event handlers:
type Role = "admin" | "editor" | "viewer";
<Select<Role>
options={[
{ value: "admin", label: "Admin" },
{ value: "editor", label: "Editor" },
]}
onChange={(role) => {
// role is typed as Role, not string
}}
/>
Verdict: Ninna UI wins for type depth and consistency. shadcn/ui types are "good enough" but degrade with customization.
7. Accessibility
Both use Radix UI primitives, so the accessibility foundation is identical. The difference is maintenance:
shadcn/ui: When Radix improves accessibility (new ARIA patterns, better screen reader support), you must manually update your copied components to benefit.
Ninna UI: Accessibility improvements ship via npm update. You get them automatically.
Verdict: Equivalent at install time. Ninna UI wins over time because updates are automatic.
Decision Framework
Choose shadcn/ui if:
- You're a solo developer or small team
- You want unlimited structural control over components
- You rarely need upstream updates
- You're comfortable managing Radix dependencies
- You enjoy customizing component internals
Choose Ninna UI if:
- You work across multiple projects
- You want standard npm maintenance (
update,audit, semver) - You prefer CSS customization over source code modification
- You want Radix accessibility without managing Radix directly
- You need consistent component behavior across your organization
Choose both if:
- You want the npm package model for most components
- But need to fork specific components for edge cases
Ninna UI's data-slot CSS API means you'll rarely need to fork. But when you do, nothing stops you from copying one component's source and customizing it.
The Bigger Picture
The copy-paste vs npm debate isn't really about shadcn/ui vs Ninna UI. It's about two different philosophies:
Copy-paste says: "Libraries are constraints. Own your code."
npm packages say: "Libraries are leverage. Own your product."
Both are valid. But the npm model has 20 years of proven infrastructure - semantic versioning, changelogs, automated updates, dependency auditing, security patches - that the copy-paste model intentionally abandons.
In 2026, with React Server Components, Tailwind CSS v4, and the increasing complexity of web accessibility requirements, the case for professional-grade npm packages has never been stronger.
Try Ninna UI
npx @ninna-ui/cli init my-app
→ Documentation
→ shadcn/ui vs Ninna UI comparison
→ All library comparisons
Top comments (0)