Skip to content

How to Add Shadcn to an Existing Project?

Written By Ajay Patel
14 min read

How to Add Shadcn to an Existing Project?

Modern frontend development is moving toward highly customizable component systems, and shadcn/ui is leading that shift. Unlike traditional UI libraries, it doesn’t ship precompiled components - instead, it gives you full control by adding editable code directly into your project.

Here’s the thing: adding shadcn/ui to an existing project is actually easier than starting fresh. No boilerplate setup, no wrestling with configs, just drop it in. In this guide, you will learn how to add Shadcn to an existing project without starting over and breaking things just to use a UI library.

What is shadcn/ui?

shadcn/ui is a set of copy-and-paste React components implemented using Radix UI primitives and Tailwind CSS. Unlike traditional UI libraries, which you install via npm, shadcn copies the components right into your codebase as .tsx files.

This means no node_modules dependencies, no version conflicts, and full control over the code. You can read it, change it, and shape it to your needs without struggling with the API of a library.

How to Add Shadcn to an Existing Project & Why?

How to Add Shadcn to an Existing Project?

Here’s the thing: Shadcn/ui doesn’t work like normal UI libraries.

When you install a component, it doesn’t add a dependency to your package.json. Instead, it copies the actual component code into your project.

The Button component isn’t coming from node_modules. It’s literally a .tsx file sitting in your components/ folder that you can open, read, and modify.

This means:

  • No conflicts with your existing setup

  • No breaking changes when shadcn updates (because you own the code)

  • No fighting with APIs when you need custom behavior

  • No bundle bloat from unused components

You’re not adopting a library. You’re getting well-written starter code that you can treat as your own.

Follow along with the code

We’ve created a repository so you can follow along: nextjs-shadcn-template

The repo has two branches:

  • starter - Basic Next.js setup with a hero section built using plain Tailwind CSS classes. This is where we’ll start.

  • final - The complete implementation after adding shadcn/ui. This is what we’ll build together.

Clone the starter branch to follow along:

git clone <https://github.com/themeselection/nextjs-shadcn-template.git>
cd nextjs-shadcn-template/starter
pnpm install
pnpm run dev

Prerequisites

Before we dive in, you should have:

  • Tailwind CSS - Basic understanding of utility classes

  • Next.js - Familiarity with the App Router

  • React - Understanding of components and props

If you’re comfortable building a basic Next.js app with Tailwind, you’re ready to go.

Initialize shadcn/ui in Your Existing Project

Alright, let’s add shadcn/ui to your project. Here’s the thing: This won’t break anything you’ve already built.

Running shadcn init

Open your terminal in your project root and run:

npx shadcn@latest init

This command does three main things:

  1. Installs required dependencies

  2. Adds a cn utility function for merging Tailwind classes

  3. Configures CSS variables in your global CSS file

The CLI will walk you through an interactive setup. Here’s what you’ll see:

Choosing directory and style options

This will ask you to select a default base color. You can select whatever you want:

“Which color would you like to use as a base color?”

I usually pick Neutral here.

› Slate
  Gray
  Zinc
  Neutral
  Stone

What files get generated

Once the init completes, shadcn creates and modifies a few files:

my-nextjs-app/
├── components.json        ← New: Configuration file
├── lib/
│   └── utils.ts          ← New: Contains cn() utility
└── app/globals.css       ← Modified: CSS variables added

Here’s what each file does:

components.json - The control center

{
  "$schema": "<https://ui.shadcn.com/schema.json>",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "iconLibrary": "lucide",
  "rtl": false,
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  },
  "registries": {}
}

lib/utils.ts - Class merging utility

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

This cn() function lets you merge Tailwind classes intelligently, handling conflicts automatically.

app/globals.css - CSS variables added

At the top of your globals.css, you’ll see new CSS variables:

These variables power your theme system. We’ll explore them more in the theming section.

Your existing code? Still works perfectly. I half-expected something to break (you know how it goes with new dependencies), but nope—everything just… worked.

Understanding components.json

The components.json file is the configuration center for shadcn/ui in your project. It tells the CLI how your project is structured and where to place components.

What this file controls

This file contains all the settings shadcn needs to work with your project:

{
  "$schema": "<https://ui.shadcn.com/schema.json>",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  }
}

Key configuration options

  • style - The design style for your components ("new-york" or "default"). This cannot be changed after initialization.

  • rsc - Enables React Server Components support. When true, the CLI automatically adds "use client" to client components.

  • tsx - Whether to use TypeScript (.tsx) or JavaScript (.jsx) files.

  • tailwind.css - Path to your global CSS file where Tailwind is imported.

  • tailwind.baseColor - The neutral color palette (slate, gray, zinc, neutral, or stone). This cannot be changed after initialization.

  • tailwind.cssVariables - Whether to use CSS variables (true) or Tailwind utility classes (false) for theming.

Path configuration

The aliases section tells shadcn where to place different types of files:

{
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui"
  }
}
  • components - Base path for your components

  • utils - Where utility functions like cn() live

  • ui - Specific directory for shadcn UI components

These aliases must match the path mappings in your tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./*"]
    }
  }
}

How shadcn knows where to place components

When you run a command like:

npx shadcn add button

The CLI:

  1. Reads components.json

  2. Looks at aliases.ui (which is @/components/ui)

  3. Creates the file at components/ui/button.tsx

  4. Imports utilities from the path specified in aliases.utils

If you want components in a different location, just update the aliases:

{
  "aliases": {
    "ui": "@/app/components/ui"  // Components will now go here
  }
}

The components.json file is just a roadmap that tells shadcn how your project is organized.

Install shadcn Components

Now that shadcn is initialized, let’s add some actual components to our project. For this tutorial, we’ll need two components: Button and Badge.

Using the add command

The add command downloads the component code and adds it directly to your project:

npx shadcn@latest add [component]

Adding Button and Badge components

Let’s add both components we need in one command:

npx shadcn@latest add button badge

Check your project structure. You should now see:

components/
└── ui/
    ├── button.tsx  ← New file added
    └── badge.tsx   ← New file added

What just happened?

When you ran this command, shadcn:

  1. Downloaded the component code from the registry

  2. Placed the files in components/ui/ (based on your components.json config)

  3. Installed any required peer dependencies (like @radix-ui/react-slot for Button)

Important: These are now your files. Open components/ui/button.tsx or components/ui/badge.tsx and you’ll see regular React component code that you can read, modify, and customize however you want.

Update page.tsx with shadcn Components

Now let’s replace the custom-styled elements with our shadcn components.

Add the imports

First, add these imports at the top of /app/page.tsx:

import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"

Replace the Badge

Find this code in your hero section:

<div className="flex items-center gap-2.5 rounded-full border bg-zinc-100 px-3 py-2 dark:bg-zinc-800">
  <span className="inline-flex items-center rounded-full border border-transparent bg-zinc-900 px-2.5 py-0.5 text-xs font-semibold text-white shadow dark:bg-zinc-50 dark:text-zinc-900">
    AI-Powered
  </span>
  <span className="text-zinc-500 dark:text-zinc-400">
    Solution for client-facing businesses
  </span>
</div>

Replace it with:

<div className='bg-muted flex items-center gap-2.5 rounded-full border px-3 py-2'>
  <Badge>AI-Powered</Badge>
  <span className='text-muted-foreground'>Solution for client-facing businesses</span>
</div>

Replace the Button

Find this code:

  href="#"
  className="inline-flex h-11 items-center justify-center rounded-md bg-zinc-900 px-8 text-sm font-medium text-white shadow transition-colors hover:bg-zinc-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90"
>
  Try It Now
</a>

Replace it with:

<Button size='lg' asChild>
  <a href='#'>Try It Now</a>
</Button>

What improved?

  • Less code: Reduced by ~70% while maintaining the same visual result

  • Design tokens: Using semantic tokens like muted and muted-foreground for automatic theme support

  • Maintainability: Components are centrally managed in components/ui/ - update once, apply everywhere

  • Accessibility: shadcn components include built-in accessibility features from Radix UI

💡While you can update the shadcn component itself, it’s best practice to avoid modifying core components. Instead, override styles using Tailwind CSS utility classes.

In the example below, we enhanced the button’s appearance by adding a subtle translate animation, along with a border and rounded corners.

<Button size='lg' asChild className="hover:ring-2 hover:ring-primary/30 hover:-translate-y-1 transition-all duration-300 rounded-full">
  <a href='#'>Try It Now</a>
</Button>

Understanding Themes in shadcn/ui

What “theme” actually means in shadcn

Here’s where shadcn gets clever. Instead of hardcoding colors like bg-blue-500, it uses design tokens

  • Instead of bg-blue-500, you use bg-primary

  • Instead of text-gray-600, you use text-muted-foreground

  • Instead of writing dark:bg-zinc-900, you just use bg-background - it automatically adapts!

These semantic tokens automatically adapt to your theme, making it trivial to switch between light/dark modes or completely change your color scheme.

How shadcn uses CSS variables

Open your app/globals.css and look at the :root section. You’ll see something like:

:root {
  --background: 0 0% 100%;
  --foreground: 240 10% 3.9%;
  --primary: 240 5.9% 10%;
  --primary-foreground: 0 0% 98%;
  /* ... more variables */
}

These are HSL color values (Hue, Saturation, Lightness) defined as CSS custom properties. When you use bg-primary in your components, Tailwind resolves it to var(--primary).

Here’s how it works: When dark mode is active, the .dark class overrides these variables with different values, and your entire UI automatically updates!

Update our Hero section with semantic classes

Let’s refactor our hero to use shadcn’s semantic tokens, eliminating the need for dark: prefixes:

Before: Manual dark mode classes

<h1 className='text-zinc-900 dark:text-zinc-50'>
  Sizzling Summer Delights
</h1>
<p className='text-zinc-600 dark:text-zinc-400'>
  Dive into a world of flavor...
</p>

After: Semantic tokens that adapt automatically

Replace your entire hero section in /app/page.tsx with:

<section className="flex min-h-[calc(100dvh-4rem)] flex-1 flex-col justify-between gap-12 overflow-x-hidden pt-8 sm:gap-16 sm:pt-16 lg:gap-24 lg:pt-24">
  {/* Hero Content */}
  <div className="mx-auto flex max-w-7xl flex-col items-center gap-8 px-4 text-center sm:px-6 lg:px-8">
    <div className="bg-muted flex items-center gap-2.5 rounded-full border px-3 py-2">
      <Badge>AI-Powered</Badge>
      <span className="text-muted-foreground">
        Solution for client-facing businesses
      </span>
    </div>

    <h1 className="text-3xl leading-[1.29167] font-bold text-balance sm:text-4xl lg:text-5xl">
      Sizzling Summer Delights
      <br />
      <span className="relative">
        Effortless
        <svg width="223" height="12" viewBox="0 0 223 12" fill="none" xmlns="<http://www.w3.org/2000/svg>" className="absolute inset-x-0 bottom-0 w-full translate-y-1/2 max-sm:hidden" > <path d="M1.11716 10.428C39.7835 4.97282 75.9074 2.70494 114.894 1.98894C143.706 1.45983 175.684 0.313587 204.212 3.31596C209.925 3.60546 215.144 4.59884 221.535 5.74551" stroke="url(#paint0_linear_10365_68643)" strokeWidth="2" strokeLinecap="round" /> <defs> <linearGradient id="paint0_linear_10365_68643" x1="18.8541" y1="3.72033" x2="42.6487" y2="66.6308" gradientUnits="userSpaceOnUse" > <stop stopColor="var(--primary)" /> <stop offset="1" stopColor="var(--primary-foreground)" /> </linearGradient> </defs> </svg>
      </span>{" "}
      Recipes for Parties!
    </h1>

    <p className="text-muted-foreground">
      Dive into a world of flavor this summer with our collection of
      Sizzling Summer Delights!
      <br />
      From refreshing appetizers to delightful desserts
    </p>

    <Button
      size="lg"
      asChild
      className="hover:ring-2 hover:ring-primary/30 hover:-translate-y-1 transition-all duration-300 rounded-full"
    >
      <a href="#">Try It Now</a>
    </Button>
  </div>

  {/* Image */}
  <img
    src="<https://cdn.shadcnstudio.com/ss-assets/blocks/marketing/hero/image-19.png>"
    alt="Dishes"
    className="min-h-67 w-full object-cover"
  />
</section>

Key changes:

  • Added text-foreground to the h1 - adapts to light/dark automatically

  • Updated SVG gradient to use hsl(var(--primary)) instead of var(--primary)

  • text-muted-foreground already handles dark mode for the paragraph

Now your hero section automatically adapts to any theme without manual dark mode classes!

Customizing Your Theme

⚠️ Important: Installing a new theme will completely replace your existing CSS variables. If you’ve customized any colors, back up your globals.css first.

Using shadcn Studio Theme Generator

The easiest way to create a custom theme is to use the Shadcn Theme Generator. This tool lets you:

  • Pick colors visually and see them applied in real-time

  • Generate both light and dark mode variants automatically

  • Export production-ready CSS variables

Let’s apply a custom theme to our project. For this tutorial, I’ll use the Spotify theme:

Customizing Your Theme

Two ways to install your theme

  1. Click the Copy button at the top right of the theme generator

  2. A dialog will appear with two installation options:

install theme

Option 1: CLI Installation (I recommend)

I prefer the CLI method; it’s faster and less error-prone. Copy the CLI command:

# For shadcn CLI v3 (latest)
pnpm dlx shadcn@latest add @ss-themes/spotify

# For shadcn CLI v2 (if you're on an older version)
pnpm dlx shadcn@latest add "<https://shadcnstudio.com/r/themes/spotify.json>"

This automatically updates your globals.css with the new theme variables.

Option 2: Manual Copy-Paste

If you prefer more control, you can manually copy the CSS variables:

  1. Click the “Copy CSS” tab in the dialog

  2. Copy the generated CSS code (it will look like this):

@layer base {
  :root {
	  --background: oklch(0.99 0 0);
	  --foreground: oklch(0.35 0.02 165.48);
	  --card: oklch(1.00 0 0);
	  --card-foreground: oklch(0.35 0.02 165.48);
	  --popover: oklch(1.00 0 0);
    /* ... more variables */
  }

  .dark {
	    --background: 0 0% 3.9%;
	    --foreground: 0 0% 98%;
	    --primary: 142.1 70.6% 45.3%;
	    --primary-foreground: 144.9 80.4% 10%;
    /* ... more variables */
  }
}
  1. Open app/globals.css in your project

  2. Find the existing :root and .dark sections (around line 10-50)

  3. Replace them with the copied CSS code

  4. Save the file

Tip: Manual installation gives you full control to tweak individual color values before applying them.

See the transformation

Your entire application now uses the Spotify theme. Here’s what changed:

  • Primary colors now use Spotify’s signature green (#1DB954)

  • Background and foreground colors match Spotify’s dark aesthetic

  • All components automatically adapt to the new theme

  • Dark mode seamlessly switches between light and dark variants

spotify theme

Before vs After:

  • The default neutral theme → vibrant Spotify green accents

  • Manual dark: classes → automatic theme adaptation

  • Single color scheme → professional, cohesive design system

Try toggling between light and dark mode to see how the theme adapts. Every shadcn component in your project now uses this new color scheme without any additional code changes!

How Light & Dark Mode Works

Now that we have our theme set up, let’s implement light and dark mode switching. We’ll follow the official shadcn documentation for Next.js dark mode.

Understanding the setup

shadcn uses next-themes to handle theme switching. This library provides:

  • Automatic system preference detection

  • Persistent theme selection (saves the user’s choice)

  • No flash of unstyled content on page load

  • Easy theme switching with a simple hook

Install next-themes

First, install the required package:

npm install next-themes

Create a Theme Provider

Create a new file components/theme-provider.tsx:

"use client"

import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

This wrapper lets us use the next-themes with Next.js App Router. It’s a necessary step because next-themes needs client-side access

Wrap your app with the provider

Open app/layout.tsx and wrap your application with the ThemeProvider:

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
						attribute="class"
						defaultTheme="light"
						enableSystem={false}
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

Key props explained:

  • attribute="class" - Adds class="dark" to the HTML element in dark mode

  • defaultTheme="light" - Respects the user’s system preference by default

  • enableSystem - Allows system preference detection

  • disableTransitionOnChange - Prevents CSS transitions when switching themes (avoids jarring animations)

Important: Add suppressHydrationWarning to the <html> tag to prevent hydration warnings from next-themes.

Create a theme toggle component

Now let’s create a button to switch between themes. Add a new file components/theme-toggle.tsx:

"use client";

import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";

import { Button } from "@/components/ui/button";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = React.useState(false);

  // Avoid hydration mismatch
  React.useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  return (
    <Button
      size="icon"
      onClick={() => setTheme(theme === "light" ? "dark" : "light")}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

This creates a button with smooth icon transitions between sun (light mode) and moon (dark mode).

Add the toggle to the page.tsx

Now let’s add the theme toggle button to the top-right corner of our page. Update app/page.tsx:

Add the import at the top:

import { ThemeToggle } from "@/components/theme-toggle"

Add the toggle button inside your page component, right after the opening <main> tag:

<div className="h-16 max-w-6xl mx-auto flex items-center">
  <ThemeToggle />
</div>

{/* Hero Section */}
<section className='flex min-h-[calc(100dvh-4rem)] flex-1 flex-col justify-between gap-12 overflow-x-hidden pt-8 sm:gap-16 sm:pt-16 lg:gap-24 lg:pt-24'>
  {/* ... rest of your hero content ... */}
</section>  

Add the toggle to the page.tsx

Test it out

Your app now has a fully functional dark mode:

  1. Look for the sun/moon icon at the top

  2. Click the theme toggle button

  3. Watch your entire UI switch between light and dark themes

  4. Your choice is saved - refresh the page, and your preference persists

  5. Try changing your system theme to see automatic detection in action

How it works behind the scenes

When you click the toggle:

  1. next-themes adds/removes the dark class from the <html> element

  2. CSS variables in your globals.css switch from :root to .dark values

  3. All shadcn components automatically use the new color values

  4. Your preference is saved to localStorage

This is why using semantic tokens like bg-background and text-foreground is so powerful - they automatically adapt without any component changes!

Conclusion

You’ve successfully added shadcn/ui to your existing Next.js project without starting from scratch. You configured the library, integrated reusable components, implemented a custom theme, and set up automatic light/dark mode switching-all while keeping full control over your codebase.

Unlike traditional component libraries that lock you into their API, shadcn gives you the source code. Every component lives in your project, where you can read it, modify it, and adapt it to your exact needs. This approach means no version conflicts, no breaking changes from upstream updates, and complete freedom to build your design system your way.

From here, explore the full range of shadcn components, experiment with new themes using the Theme Generator, or start building your own custom variants. The components you’ve added are just the beginning.

Happy building! 🚀