Join our Discord Community
Installation

Install shadcn/ui Manually

How to install shadcn/ui manually in any React project. Custom setup guide for webpack, parcel, and other build tools.

How to install shadcn/ui manually

Sometimes the CLI doesn't cover your setup. Maybe you're using a custom build tool, integrating with an existing design system, or just prefer doing things by hand. Here's exactly what shadcn/ui needs to work.

Core requirements

Before starting, make sure you have:

  • React 18 or higher
  • Tailwind CSS installed and working
  • A build tool that handles TypeScript/JSX

Step-by-step setup

Install dependencies

pnpm add class-variance-authority clsx tailwind-merge lucide-react tw-animate-css

What each package does:

  • class-variance-authority - Type-safe component variants
  • clsx - Conditional classNames
  • tailwind-merge - Merge Tailwind classes without conflicts
  • lucide-react - Icon library used by components
  • tw-animate-css - Additional animation utilities

Configure path aliases

Add to your tsconfig.json:

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

Adjust the path to match your project structure. This enables clean imports.

Also configure your bundler to understand these paths:

Webpack:

resolve: {
  alias: {
    '@': path.resolve(__dirname, './src')
  }
}

Rollup/Vite:

resolve: {
  alias: {
    '@': './src'
  }
}

Add CSS variables and Tailwind config

Create or update your global CSS file:

@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --card: oklch(1 0 0);
  --card-foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --popover-foreground: oklch(0.145 0 0);
  --primary: oklch(0.205 0 0);
  --primary-foreground: oklch(0.985 0 0);
  --secondary: oklch(0.97 0 0);
  --secondary-foreground: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --muted-foreground: oklch(0.556 0 0);
  --accent: oklch(0.97 0 0);
  --accent-foreground: oklch(0.205 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.625rem;
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
  --card: oklch(0.145 0 0);
  --card-foreground: oklch(0.985 0 0);
  --popover: oklch(0.145 0 0);
  --popover-foreground: oklch(0.985 0 0);
  --primary: oklch(0.985 0 0);
  --primary-foreground: oklch(0.205 0 0);
  --secondary: oklch(0.269 0 0);
  --secondary-foreground: oklch(0.985 0 0);
  --muted: oklch(0.269 0 0);
  --muted-foreground: oklch(0.708 0 0);
  --accent: oklch(0.269 0 0);
  --accent-foreground: oklch(0.985 0 0);
  --destructive: oklch(0.396 0.141 25.723);
  --destructive-foreground: oklch(0.985 0 0);
  --border: oklch(0.269 0 0);
  --input: oklch(0.269 0 0);
  --ring: oklch(0.439 0 0);
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-primary: var(--primary);
  --color-primary-foreground: var(--primary-foreground);
  --color-border: var(--border);
  --color-ring: var(--ring);
  /* Map all other variables */
}

@layer base {
  * {
    @apply border-border outline-ring/50;
  }
  body {
    @apply bg-background text-foreground;
  }
}

Create the cn utility

Create lib/utils.ts:

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

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

This utility merges classes intelligently, handling Tailwind conflicts.


Create components.json

This file tells the CLI about your project structure:

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

Adjust paths to match your project.


Add your first component

Now you can either:

Use the CLI:

npx shadcn@latest add button

Or copy manually:

Create 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 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    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 hover:text-accent-foreground",
        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 }

Verifying your setup

Test that everything works:

// App.tsx
import { Button } from "@/components/ui/button"

function App() {
  return (
    <div className="p-8">
      <Button>It works!</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="destructive">Destructive</Button>
    </div>
  )
}

If the buttons render with proper styling, you're good to go.

Framework-specific notes

Create React App

CRA doesn't support path aliases without ejecting. Use relative imports or craco:

// craco.config.js
module.exports = {
  webpack: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

Parcel

Parcel needs an .parcelrc:

{
  "extends": "@parcel/config-default",
  "resolvers": ["@parcel/resolver-default"],
  "transformers": {
    "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"]
  }
}

Webpack 5

Add to your webpack config:

resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src')
  },
  extensions: ['.ts', '.tsx', '.js', '.jsx']
}

Customization options

Custom color palette

Replace the CSS variables with your brand colors:

:root {
  --primary: oklch(0.5 0.3 250); /* Your brand blue */
  --secondary: oklch(0.7 0.2 120); /* Your brand green */
}

Add a CSS prefix

To avoid conflicts, add a prefix in components.json:

{
  "tailwind": {
    "prefix": "ui-"
  }
}

Now classes become ui-bg-primary instead of bg-primary.

Different icon library

Don't want Lucide? Install your preferred library and update imports:

// Using Heroicons instead
import { CheckIcon } from '@heroicons/react/24/outline'

Troubleshooting

Classes not applying

Check that:

  1. Tailwind CSS is processing your component files
  2. CSS variables are defined in your global CSS
  3. The cn() utility is imported correctly

TypeScript errors

Make sure your tsconfig.json includes:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Build errors

Common fixes:

  • Ensure all peer dependencies are installed
  • Check that your bundler handles .tsx files
  • Verify path aliases are configured in both TypeScript and your bundler

What's next

With manual setup complete:

  1. Browse official components for forms, tables, and UI elements
  2. Add charts for data visualization
  3. Explore community components for extended functionality
  4. Add useful hooks to enhance your components
  5. Use pre-built blocks to quickly build common layouts

The manual approach gives you complete control over every aspect of the setup. Perfect for when you need shadcn/ui to fit into your existing architecture exactly how you want it.

Questions you're probably thinking