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
# ❌ Wrong - no npm package exists npm install shadcn-ui # ✅ Correct - copy component into project npx shadcn@latest add button
Component Installation Command
Use the shadcn CLI to add components:
# Add single component npx shadcn@latest add button # Add multiple components npx shadcn@latest add button input card # Add all components (not recommended) 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:
/* app/globals.css */
@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%;
/* ... more variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... more variables */
}
}
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]:
// ✅ Correct
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
// ❌ Wrong - relative paths
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:
// components/ui/button.tsx
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
// app/page.tsx
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%; /* Current primary color */
}
Step 2: Update Color Value
Change to desired color (use HSL format):
:root {
--primary: 263 70% 50%; /* New purple primary */
}
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%; /* Lighter purple for dark mode */
}
Step 4: Test Across App
Verify all primary-colored components look correct.
Workflow 3: Create Custom Variant
Step 1: Locate Component File
// components/ui/button.tsx
const buttonVariants = cva("...", {
variants: {
variant: {
default: "...",
destructive: "...",
outline: "...",
// Add custom variant here
},
},
});
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",
// Custom success variant
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",
// Custom extra-large size
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%;
/* ... all color variables */
}
Step 2: Add Theme Provider (if using next-themes)
// app/layout.tsx
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}", // Must include this
],
// ...
};
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%;
/* ... all required variables */
}
}
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%;
/* ... all other variables */
}
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:
// ❌ Wrong - classes override each other
<Button className="bg-red-500">Delete</Button>
// ✅ Correct - modify component to accept custom classes properly
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:
- • Component installed via
npx shadcn@latest add - • Component renders without errors
- • Styles applied correctly
- • Variants work as expected
- • Dark mode works (if implemented)
- • Import alias (@/*) working
- • Tailwind config includes component paths
- • CSS variables defined in globals.css
- • Component accessible and keyboard-navigable
- • Responsive on mobile devices
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:
// components/forms/SearchBox.tsx
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:
// lib/theme.ts
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
- •Previous expertise:
.claude/experts/shadcn-expert/expertise.yaml - •Agent integration:
.claude/agents/agent-shadcn.md - •Official docs: https://ui.shadcn.com
- •Tailwind docs: https://tailwindcss.com