Back

How to Integrate ShadCN with Next.js

How to Integrate ShadCN with Next.js

Setting up a modern UI component library shouldn’t feel like assembling furniture without instructions. If you’re building with Next.js 15 and want beautiful, customizable components without the black-box frustration of traditional libraries, this guide shows you exactly how to integrate Shadcn UI into your project.

This article covers the complete Shadcn UI Next.js setup process, from initial project creation through component installation, Tailwind CSS configuration, and proper dark mode implementation. You’ll learn how to avoid common React 19 compatibility issues and set up a production-ready component system that you actually own.

Key Takeaways

  • Shadcn UI copies component source code directly into your project, giving you complete ownership and customization control
  • The CLI handles installation and dependency management, making setup straightforward even with React 19 compatibility considerations
  • Tailwind CSS integration is seamless, with built-in theming support and dark mode implementation
  • Components are framework-agnostic and work with both App Router and Pages Router in Next.js

Why Choose Shadcn UI for Your Next.js Project

Shadcn UI takes a fundamentally different approach to component libraries. Instead of installing a dependency you can’t modify, Shadcn copies component source code directly into your project. This means you get:

  • Complete ownership of your components - modify anything, anytime
  • Tailwind-first design that integrates seamlessly with your existing styles
  • Zero runtime overhead - components are just code in your project
  • Type safety built in from the ground up
  • Accessibility handled correctly by default through Radix UI primitives

This approach particularly shines with Next.js 15’s App Router, where server components and client boundaries require careful component design.

Prerequisites and Initial Setup

Before starting your Shadcn UI Next.js setup, ensure you have:

  • Node.js 18.17 or higher
  • A package manager (npm, pnpm, yarn, or bun)
  • Basic familiarity with React and Tailwind CSS

Creating Your Next.js 15 Project

Start by creating a new Next.js project with TypeScript and Tailwind CSS:

npx create-next-app@latest my-app --typescript --tailwind --app

When prompted, select these options:

  • ✓ Would you like to use ESLint? Yes
  • ✓ Would you like to use src/ directory? Yes (recommended)
  • ✓ Would you like to customize the default import alias? No

Navigate to your project:

cd my-app

Installing and Configuring Shadcn UI

The Shadcn CLI handles most of the heavy lifting for component installation. Run the initialization command:

npx shadcn@latest init

You’ll see several configuration prompts:

  1. Style: Choose your preferred style (Default/New York)
  2. Base color: Select from available color schemes
  3. CSS variables: Use Yes for better theming support

Handling React 19 Peer Dependencies

If you’re using npm with React 19, you’ll encounter peer dependency warnings:

npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree

The CLI will prompt you to choose a resolution strategy:

? How would you like to proceed?
 Use --force
  Use --legacy-peer-deps

Select --legacy-peer-deps for the safest approach. Users of pnpm, yarn, or bun won’t see these warnings and can proceed normally.

Adding Your First Components

With Shadcn initialized, add components using the CLI:

npx shadcn@latest add button card

This command:

  1. Downloads the component source code
  2. Places it in src/components/ui/
  3. Installs any required dependencies (like Radix UI primitives)
  4. Updates your component imports

Here’s what a typical Shadcn button component looks like after installation:

// src/components/ui/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 transition-colors focus-visible:outline-none focus-visible:ring-2",
  {
    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",
        secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        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",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }

Notice how the component is fully customizable - you can modify variants, add new ones, or change the styling completely.

Configuring Tailwind CSS for Shadcn

Shadcn UI works with both Tailwind CSS v3 and v4. The CLI automatically configures your tailwind.config.js during initialization. Here’s what gets added:

// tailwind.config.js
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  theme: {
    container: {
      center: true,
      padding: "2rem",
      screens: {
        "2xl": "1400px",
      },
    },
    extend: {
      colors: {
        border: "hsl(var(--border))",
        input: "hsl(var(--input))",
        ring: "hsl(var(--ring))",
        background: "hsl(var(--background))",
        foreground: "hsl(var(--foreground))",
        primary: {
          DEFAULT: "hsl(var(--primary))",
          foreground: "hsl(var(--primary-foreground))",
        },
        secondary: {
          DEFAULT: "hsl(var(--secondary))",
          foreground: "hsl(var(--secondary-foreground))",
        },
        destructive: {
          DEFAULT: "hsl(var(--destructive))",
          foreground: "hsl(var(--destructive-foreground))",
        },
        muted: {
          DEFAULT: "hsl(var(--muted))",
          foreground: "hsl(var(--muted-foreground))",
        },
        accent: {
          DEFAULT: "hsl(var(--accent))",
          foreground: "hsl(var(--accent-foreground))",
        },
        popover: {
          DEFAULT: "hsl(var(--popover))",
          foreground: "hsl(var(--popover-foreground))",
        },
        card: {
          DEFAULT: "hsl(var(--card))",
          foreground: "hsl(var(--card-foreground))",
        },
      },
      borderRadius: {
        lg: "var(--radius)",
        md: "calc(var(--radius) - 2px)",
        sm: "calc(var(--radius) - 4px)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}

Update Your globals.css

The CLI also updates your globals.css with CSS variables for theming:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 84% 4.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96%;
    --secondary-foreground: 222.2 84% 4.9%;
    --muted: 210 40% 96%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96%;
    --accent-foreground: 222.2 84% 4.9%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 210 40% 98%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 84% 4.9%;
    --secondary: 217.2 32.6% 17.5%;
    --secondary-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --accent: 217.2 32.6% 17.5%;
    --accent-foreground: 210 40% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 210 40% 98%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 224.3 76.3% 94.1%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}

Implementing Dark Mode with next-themes

For a production-ready dark mode implementation, use next-themes to avoid hydration issues.

Install next-themes

npm install next-themes

Create a Theme Provider

Create src/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>
}

Update Your Root Layout

Modify src/app/layout.tsx:

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

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

Create a Theme Toggle Component

"use client"

import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"

export function ThemeToggle() {
  const { setTheme, theme } = useTheme()

  return (
    <Button
      variant="ghost"
      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>
  )
}

Working with React 19 Changes

React 19 introduces several changes that affect how you work with Shadcn components:

Simplified ref Handling

React 19 allows passing ref as a regular prop, eliminating the need for forwardRef in many cases:

// Old approach (React 18)
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, ...props }, ref) => {
    return <input ref={ref} className={className} {...props} />
  }
)

// New approach (React 19)
function Input({ className, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} className={className} {...props} />
}

New Form Hooks

React 19’s useActionState and useFormStatus hooks work seamlessly with Shadcn form components:

import { useActionState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

function ContactForm() {
  const [state, formAction] = useActionState(async (prevState, formData) => {
    // Server action logic here
    const email = formData.get("email")
    // Process form...
    return { success: true }
  }, { success: false })

  return (
    <form action={formAction}>
      <Input name="email" type="email" required />
      <Button type="submit">Submit</Button>
      {state.success && <p>Thanks for subscribing!</p>}
    </form>
  )
}

Common Issues and Solutions

Build Errors with Tailwind CSS

If you encounter build errors after setup, ensure your PostCSS configuration is correct:

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

Component Styling Not Applied

Check that your globals.css is imported in your root layout:

import "./globals.css"

TypeScript Errors with Component Props

Ensure your tsconfig.json includes the correct paths:

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

Best Practices for Production

  1. Customize the cn utility: The cn function in lib/utils.ts merges classes intelligently. Extend it for your specific needs.

  2. Create component variants: Use CVA (class-variance-authority) to create consistent component variants across your application.

  3. Optimize bundle size: Only install components you actually use. Each component is independent.

  4. Test accessibility: Shadcn components use Radix UI primitives which handle accessibility, but always test with screen readers.

  5. Version control: Since components are copied into your project, commit them to version control and track changes.

Conclusion

Setting up Shadcn UI with Next.js gives you a modern component system that balances flexibility with development speed. Unlike traditional component libraries, you own every line of code, can customize anything, and aren’t locked into someone else’s design decisions.

The combination of Next.js 15’s App Router, React 19’s improvements, and Tailwind CSS creates a powerful foundation for building modern web applications. With Shadcn’s approach of copying components rather than installing them as dependencies, you get the best of both worlds: rapid development and complete control.

FAQs

When using npm with React 19, you'll need to use the --legacy-peer-deps flag during component installation due to peer dependency conflicts. Package managers like pnpm, yarn, and bun handle these dependencies more gracefully and don't require any special flags. The end result is the same regardless of which package manager you choose.

Yes, Shadcn UI works with both App Router and Pages Router. The component code itself is framework-agnostic. The main difference is in how you implement providers like the theme provider - in Pages Router, you'd wrap your app in _app.tsx instead of the root layout.

Since Shadcn copies components directly into your project, you can modify them like any other code. Open the component file in src/components/ui/, adjust the Tailwind classes or CVA variants, and save. You can also modify the global theme variables in your CSS to change colors and spacing across all components at once.

No, you should only install components as you need them. Each component is independent and includes its own dependencies. This keeps your bundle size minimal and your project organized. Use npx shadcn@latest add [component-name] to add individual components when needed.

Since components are copied into your project, they won't automatically update when Shadcn releases new versions. You can manually check the Shadcn UI documentation for updates and either re-run the add command for specific components or manually apply changes. This gives you control over when and how components are updated in your project.

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers