Migrating from shadcn/ui to Ninna UI: A Practical Guide
A step-by-step walkthrough of moving a real React app off copy-pasted shadcn/ui components onto versioned Ninna UI packages — without losing Radix accessibility.
By Ninna UI Team
shadcn/ui popularised an idea that genuinely changed front-end development: instead of installing a component library, you copy its source into your repo and own it. It's a great model — until you've copied 40 components, customised half of them, and every `npx shadcn add` turns into a merge conflict. This guide walks through moving a real app to Ninna UI, which keeps the same Radix + Tailwind foundation but ships as versioned npm packages.
You don't have to migrate everything at once. Ninna UI and shadcn/ui components are both just React + Tailwind, so they coexist happily while you migrate page by page.
1. Install the packages you need
Where shadcn/ui drops files into components/ui, Ninna UI gives you scoped packages. Install the core theme plus whichever component packages you use:
npm install @ninna-ui/core @ninna-ui/primitives @ninna-ui/overlays @ninna-ui/forms2. Replace one CSS import for all theming
Delete your hand-managed HSL variables and components.json. Ninna UI theming is a single CSS import with oklch perceptual colors:
@import "tailwindcss";
@import "@ninna-ui/core/theme/presets/default.css";Dark mode now works through CSS — there's no color-mode script or provider to wire up.
3. Swap imports component by component
Most components map one-to-one. Replace the copy-pasted import path with the package import:
// Before (shadcn/ui)
import { Button } from "@/components/ui/button";
// After (Ninna UI)
import { Button } from "@ninna-ui/primitives";Overlays gotcha: Modal, Drawer, Popover, Tooltip, and DropdownMenu live in @ninna-ui/overlays — not @ninna-ui/primitives. This trips up both humans and AI agents.
4. Drop the Radix peer dependencies
shadcn/ui has you install @radix-ui/* packages directly. Ninna UI wraps Radix internally through @ninna-ui/react-internal, so you can remove those peer deps — their types never leak into your code.
5. Customise through data-slot CSS, not source edits
This is the biggest mental shift. You no longer own the source, so you customise through CSS. Ninna UI exposes 98 data-slot targets plus the cn() helper:
<Button className="data-[slot=button]:rounded-full">Rounded</Button>What you gain
- Bug fixes and a11y patches arrive via npm update — no manual re-copy or merge conflicts.
- A cleaner dependency tree with no Radix peer deps to manage.
- oklch perceptual colors instead of hand-tuned HSL variables.
- Smaller surface area to maintain in your own repo.
What you give up
Honesty matters: you lose direct source ownership. If editing a component's internals line-by-line is core to your workflow, shadcn/ui may still suit you better. For most teams, trading source ownership for zero-maintenance updates is a clear win.