| name | shadcn/ui Component Integration |
| description | Install and customize shadcn/ui components with Tailwind CSS theming, variants, and styling. Use when adding UI components or customizing the design system. |
| tools | Read, Grep, Glob, Write, Edit, Bash |
| model | inherit |
Skill: shadcn/ui Component Integration
Complete guide for installing, customizing, and styling shadcn/ui components with Tailwind CSS theming.
When to Use
- Adding new shadcn/ui components to your project
- Customizing component styles and variants
- Setting up or modifying theme configuration
- Troubleshooting component styling issues
- Creating custom component variants
- Integrating Tailwind CSS with shadcn/ui
Domain Knowledge
Critical Patterns
Copy-Paste, Not NPM Install (CRITICAL)
shadcn/ui is NOT an npm package - it's a collection of reusable components that you copy into your project.
Installation method:
npx shadcn@latest add [component-name]
What this does:
- Downloads component source code
- Places it in
components/ui/[component-name].tsx
- You own the code and can modify it directly
Why this matters:
- Components live in YOUR codebase
- Full control over styling and behavior
- No hidden abstractions or locked-in APIs
- Customize directly in the component file
npm install shadcn-ui
npx shadcn@latest add button
Component Installation Command
Use the shadcn CLI to add components:
npx shadcn@latest add button
npx shadcn@latest add button input card
npx shadcn@latest add --all
Components are copied to components/ui/ directory.
CSS Variables for Theming
shadcn/ui uses CSS variables defined in app/globals.css for theming:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
}
Format: HSL values without the hsl() wrapper.
Why: Allows easy theme switching between light/dark modes by changing CSS classes.
Import Path Convention
Always import components from @/components/ui/[component]:
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Button } from "../../components/ui/button";
Why: The @/* alias is configured in tsconfig.json and provides clean imports.
Key Files
- components/ui/ - All shadcn/ui components live here
- app/globals.css - CSS variables for theming
- tailwind.config.ts - Tailwind configuration
- lib/utils.ts - Utility functions (like
cn() for class merging)
- components.json - shadcn/ui configuration
Component Anatomy
All shadcn/ui components follow similar patterns:
import * as React from "react"
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
export { Button, buttonVariants }
Key parts:
cva() - Creates variant classes
cn() - Merges classes intelligently
VariantProps - TypeScript types for variants
- Variants system - Predefined style combinations
Theming System
shadcn/ui uses Tailwind CSS with custom color tokens:
Color tokens:
background / foreground - Base colors
primary / primary-foreground - Primary actions
secondary / secondary-foreground - Secondary actions
muted / muted-foreground - Muted content
accent / accent-foreground - Accent highlights
destructive / destructive-foreground - Destructive actions
border - Border color
input - Input border color
ring - Focus ring color
Use in Tailwind classes:
<div className="bg-primary text-primary-foreground">
Primary colored section
</div>
Workflows
Workflow 1: Add New Component
Step 1: Install Component
npx shadcn@latest add button
This creates components/ui/button.tsx.
Step 2: Import and Use
import { Button } from "@/components/ui/button";
export default function Page() {
return (
<div>
<Button>Click me</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive">Delete</Button>
</div>
);
}
Step 3: Verify Styling
Check that:
- Component renders correctly
- Styles are applied
- Dark mode works (if implemented)
- Variants work as expected
Workflow 2: Customize Component Theme
Step 1: Identify CSS Variable
Find the variable in app/globals.css:
:root {
--primary: 222.2 47.4% 11.2%;
}
Step 2: Update Color Value
Change to desired color (use HSL format):
:root {
--primary: 263 70% 50%;
}
Convert colors to HSL:
- Use online converter or browser DevTools
- Format:
hue saturation% lightness%
- Example: Purple =
263 70% 50%
Step 3: Update Dark Mode (Optional)
.dark {
--primary: 263 75% 60%;
}
Step 4: Test Across App
Verify all primary-colored components look correct.
Workflow 3: Create Custom Variant
Step 1: Locate Component File
const buttonVariants = cva(
"...",
{
variants: {
variant: {
default: "...",
destructive: "...",
outline: "...",
},
},
}
)
Step 2: Add New Variant
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
success: "bg-green-600 text-white hover:bg-green-700",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
xl: "h-14 rounded-md px-12 text-lg",
},
},
}
)
Step 3: Use Custom Variant
<Button variant="success">Save Changes</Button>
<Button size="xl">Large Button</Button>
Workflow 4: Combine Multiple Components
Step 1: Install Required Components
npx shadcn@latest add card button input label
Step 2: Compose Components
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function LoginCard() {
return (
<Card className="w-[350px]">
<CardHeader>
<CardTitle>Login</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
<Button className="w-full">Sign In</Button>
</form>
</CardContent>
</Card>
);
}
Step 3: Apply Consistent Spacing
Use Tailwind utility classes for consistent spacing:
space-y-4 - Vertical spacing between children
gap-4 - Grid/flex gap
p-4 - Padding
m-4 - Margin
Workflow 5: Implement Dark Mode
Step 1: Ensure Dark Mode Variables Exist
Check app/globals.css has dark mode styles:
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
}
Step 2: Add Theme Provider (if using next-themes)
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
);
}
Step 3: Create Theme Toggle
"use client";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<Button
variant="ghost"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
Toggle theme
</Button>
);
}
Step 4: Test Both Themes
Verify all components look good in light and dark modes.
Troubleshooting
Issue: Component Not Found After Installation
Symptoms:
- Import error: "Cannot find module '@/components/ui/button'"
- Component file exists but import fails
Cause: @/* import alias not configured
Solution:
Check tsconfig.json has the alias:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
Also check next.config.ts or vite.config.ts depending on your setup.
Frequency: Medium
Issue: Styling Not Applied
Symptoms:
- Component renders but has no styling
- Looks like unstyled HTML
Possible Causes & Solutions:
1. Tailwind not processing component paths
Check tailwind.config.ts:
export default {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
}
2. CSS variables missing
Check app/globals.css has all CSS variables:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
}
}
3. Tailwind directives missing
Ensure app/globals.css has:
@tailwind base;
@tailwind components;
@tailwind utilities;
Frequency: Medium
Issue: Dark Mode Not Working
Symptoms:
- Theme toggle doesn't change colors
- Dark class applied but no visual change
Cause: Missing dark mode CSS variables
Solution:
Ensure app/globals.css has complete dark mode section:
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
}
Variables must match the light mode variables (same names).
Frequency: Low
Issue: Class Names Not Merging Correctly
Symptoms:
- Custom classes override all styles
- Variants not working with additional classes
Cause: Not using cn() utility properly
Solution:
Always use cn() to merge classes:
<Button className="bg-red-500">Delete</Button>
import { cn } from "@/lib/utils";
<Button className={cn("bg-red-500")}>Delete</Button>
Or modify the variant instead:
<Button variant="destructive">Delete</Button>
Frequency: Low
Validation Checklist
Before considering shadcn/ui integration complete:
Best Practices
- Install only what you need - Don't use
--all, add components as needed
- Customize after installation - Components are meant to be modified
- Use variants over custom classes - Prefer variants for consistency
- Maintain the theme system - Use CSS variables, not hardcoded colors
- Test dark mode early - Don't wait until the end to implement dark mode
- Keep components/ui clean - Only shadcn components in this folder
- Create wrapper components - Build app-specific variants in separate folder
- Use consistent spacing - Leverage Tailwind's spacing scale
Advanced Techniques
Creating Composite Components
Build higher-level components from shadcn primitives:
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Search } from "lucide-react";
export function SearchBox() {
return (
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search..."
className="pl-10"
/>
<Button
size="sm"
className="absolute right-1 top-1/2 -translate-y-1/2"
>
Search
</Button>
</div>
);
}
Theming Multiple Projects
Extract theme to separate file:
export const theme = {
light: {
background: "0 0% 100%",
foreground: "222.2 84% 4.9%",
},
dark: {
background: "222.2 84% 4.9%",
foreground: "210 40% 98%",
},
};
Generate globals.css from theme object for consistency across projects.
References