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:
- Tailwind CSS is processing your component files
- CSS variables are defined in your global CSS
- 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:
- Browse official components for forms, tables, and UI elements
- Add charts for data visualization
- Explore community components for extended functionality
- Add useful hooks to enhance your components
- 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.