shadcn/ui Setup Skill
This skill covers shadcn/ui component library installation and customization.
When to Use
Use this skill when:
- •Need pre-styled accessible components
- •Building design systems quickly
- •Want full control over component code
- •Using Tailwind CSS
Core Principle
COPY, DON'T IMPORT - shadcn/ui components are copied into your codebase. You own and customize them fully.
Installation
Initialize Project
bash
# For new Next.js project npx create-next-app@latest my-app --typescript --tailwind --eslint # Initialize shadcn/ui npx shadcn@latest init
Configuration Options
text
Would you like to use TypeScript? yes Which style would you like to use? Default Which color would you like to use as base color? Slate Where is your global CSS file? app/globals.css Would you like to use CSS variables for colors? yes Where is your tailwind.config.ts located? tailwind.config.ts Configure the import alias for components: @/components Configure the import alias for utils: @/lib/utils Are you using React Server Components? yes
Generated Configuration
json
// components.json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
Adding Components
bash
# Add individual components npx shadcn@latest add button npx shadcn@latest add card npx shadcn@latest add dialog npx shadcn@latest add dropdown-menu npx shadcn@latest add form npx shadcn@latest add input npx shadcn@latest add select npx shadcn@latest add tabs npx shadcn@latest add toast # Add multiple components npx shadcn@latest add button card dialog # Add all components npx shadcn@latest add --all
Component Structure
code
src/
├── components/
│ └── ui/
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ └── ...
├── lib/
│ └── utils.ts
└── app/
└── globals.css
Utility Function
typescript
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
Using Components
Button
typescript
import { Button } from '@/components/ui/button';
function App(): React.ReactElement {
return (
<div className="flex gap-2">
<Button variant="default">Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
<Button disabled>Disabled</Button>
<Button asChild>
<a href="/about">Link as Button</a>
</Button>
</div>
);
}
Card
typescript
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
function UserCard({ user }: { user: User }): React.ReactElement {
return (
<Card>
<CardHeader>
<CardTitle>{user.name}</CardTitle>
<CardDescription>{user.email}</CardDescription>
</CardHeader>
<CardContent>
<p>{user.bio}</p>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</CardFooter>
</Card>
);
}
Dialog
typescript
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
function ConfirmDialog(): React.ReactElement {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Delete</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Form with React Hook Form + Zod
typescript
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
});
type FormValues = z.infer<typeof formSchema>;
function ProfileForm(): React.ReactElement {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: '',
email: '',
},
});
function onSubmit(values: FormValues): void {
console.log(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
Your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
);
}
Customizing Components
Adding Variants
typescript
// components/ui/button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
// Add custom variants
success: 'bg-green-500 text-white hover:bg-green-600',
warning: 'bg-yellow-500 text-white hover:bg-yellow-600',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
// Add custom sizes
xl: 'h-12 rounded-md px-10 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
);
Customizing Theme Colors
css
/* 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%;
/* Add custom colors */
--success: 142 76% 36%;
--success-foreground: 0 0% 100%;
--warning: 38 92% 50%;
--warning-foreground: 0 0% 100%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* Dark mode colors */
}
}
Best Practices
- •Don't modify node_modules - Components are in your codebase
- •Use cn() utility - For conditional classes
- •Keep variants consistent - Follow existing patterns
- •Customize at theme level - Use CSS variables
- •Document customizations - For team consistency
Common Patterns
Loading Button
typescript
<Button disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? 'Loading...' : 'Submit'}
</Button>
Responsive Dialog
typescript
import {
Dialog,
DialogContent,
} from '@/components/ui/dialog';
import {
Drawer,
DrawerContent,
} from '@/components/ui/drawer';
import { useMediaQuery } from '@/hooks/use-media-query';
function ResponsiveDialog({ open, onOpenChange, children }: Props): React.ReactElement {
const isDesktop = useMediaQuery('(min-width: 768px)');
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>{children}</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={onOpenChange}>
<DrawerContent>{children}</DrawerContent>
</Drawer>
);
}
Notes
- •shadcn/ui is built on Radix UI primitives
- •All components are fully accessible
- •Works with React Server Components
- •No runtime dependency (code is yours)