shadcn
azlekov/my-claude-code/skills/shadcn/SKILL.md
This skill should be used when the user asks to "add a component", "use shadcn", "install Button", "create Dialog", "add Form", "use DataTable", "implement dark mode toggle", "use cn utility", or discusses UI components, component libraries, or accessible components. Always use the latest shadcn/ui version and modern patterns.
Skill1 starsChanged 9 months ago
What's in it
- shadcn/ui Development
- Quick Reference
- Installation
- Initialize in Next.js
- Add Components
- The cn() Utility
- Core Components
- Button
- Card
- Dialog
- Input & Label
- Select
- Form Handling
- With React Hook Form + Zod
- With Server Actions
- Dark Mode
- Theme Provider Setup
- Theme Toggle
- Toast Notifications (Sonner)
- Common Patterns
- Responsive Sheet/Dialog
- Loading Button
- Additional Resources
---
name: shadcn
description: This skill should be used when the user asks to "add a component", "use shadcn", "install Button", "create Dialog", "add Form", "use DataTable", "implement dark mode toggle", "use cn utility", or discusses UI components, component libraries, or accessible components. Always use the latest shadcn/ui version and modern patterns.
version: 1.0.0
---
# shadcn/ui Development
This skill provides guidance for building interfaces with shadcn/ui, focusing on **always using the latest version** and modern patterns.
> **Philosophy:** Copy and own your components. Use the `new-york` style. Leverage Radix UI primitives for accessibility.
## Quick Reference
| Feature | Modern Approach | Legacy (Avoid) |
|---------|----------------|----------------|
| Style | `new-york` | `default` (deprecated) |
| Toast | `sonner` | `toast` component |
| Animation | CSS/tw-animate-css | `tailwindcss-animate` |
| forwardRef | Direct `ref` prop (React 19) | `forwardRef` wrapper |
## Installation
### Initialize in Next.js
```bash
npx shadcn@latest init
```
Configuration prompts:
- Style: **new-york** (recommended)
- Base color: neutral, slate, zinc, gray, or stone
- CSS variables: **Yes** (recommended)
### Add Components
```bash
# Add individual components
npx shadcn@latest add button
npx shadcn@latest add card dialog form input
# Add multiple components
npx shadcn@latest add button card dialog form input label textarea
```
## The cn() Utility
Merge Tailwind classes conditionally:
```tsx
import { cn } from "@/lib/utils"
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
}
export function Button({
className,
variant = 'default',
size = 'md',
...props
}: ButtonProps) {
return (
<button
className={cn(
// Base styles
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-50",
// Variants
variant === 'default' && "bg-primary text-primary-foreground hover:bg-primary/90",
variant === 'destructive' && "bg-destructive text-destructive-foreground hover:bg-destructive/90",
variant === 'outline' && "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
// Sizes
size === 'sm' && "h-8 px-3 text-xs",
size === 'md' && "h-10 px-4 text-sm",
size === 'lg' && "h-12 px-6 text-base",
// Custom classes
className
)}
{...props}
/>
)
}
```
## Core Components
### Button
```tsx
import { Button } from "@/components/ui/button"
// Variants
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Destructive</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><IconSearch /></Button>
// States
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/about">As Link</Link>
</Button>
```
### Card
```tsx
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
```
### Dialog
```tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="py-4">
Dialog body content
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
### Input & Label
```tsx
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
/>
</div>
```
### Select
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
<Select>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select option" />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
<SelectItem value="option3">Option 3</SelectItem>
</SelectContent>
</Select>
```
## Form Handling
### With React Hook Form + Zod
```tsx
'use client'
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as 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, "Username must be at least 2 characters"),
email: z.string().email("Invalid email address"),
})
type FormValues = z.infer<typeof formSchema>
export function ProfileForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: FormValues) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<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" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
```
### With Server Actions
```tsx
'use client'
import { useActionState } from 'react'
import { createUser } from './actions'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function SignupForm() {
const [state, formAction, isPending] = useActionState(createUser, {
error: null
})
return (
<form action={formAction} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
disabled={isPending}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
disabled={isPending}
/>
</div>
{state.error && (
<p className="text-sm text-destructive">{state.error}</p>
)}
<Button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Account'}
</Button>
</form>
)
}
```
## Dark Mode
### Theme Provider Setup
```tsx
// components/theme-provider.tsx
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
```
```tsx
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
```
### Theme Toggle
```tsx
'use client'
import { useTheme } from 'next-themes'
import { Button } from "@/components/ui/button"
import { Moon, Sun } from "lucide-react"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
```
## Toast Notifications (Sonner)
```tsx
// app/layout.tsx
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
)
}
```
```tsx
// In components
import { toast } from "sonner"
function MyComponent() {
return (
<Button
onClick={() => {
toast.success("Success!", {
description: "Your changes have been saved."
})
}}
>
Save
</Button>
)
}
// Other toast types
toast("Default toast")
toast.success("Success message")
toast.error("Error message")
toast.warning("Warning message")
toast.info("Info message")
toast.loading("Loading...")
// With action
toast("Event created", {
action: {
label: "Undo",
onClick: () => console.log("Undo")
}
})
// Promise-based
toast.promise(saveData(), {
loading: "Saving...",
success: "Saved!",
error: "Error saving"
})
```
## Common Patterns
### Responsive Sheet/Dialog
```tsx
'use client'
import { useMediaQuery } from "@/hooks/use-media-query"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet"
interface ResponsiveModalProps {
open: boolean
onOpenChange: (open: boolean) => void
title: string
children: React.ReactNode
}
export function ResponsiveModal({
open,
onOpenChange,
title,
children
}: ResponsiveModalProps) {
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
{children}
</DialogContent>
</Dialog>
)
}
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
{children}
</SheetContent>
</Sheet>
)
}
```
### Loading Button
```tsx
import { Loader2 } from "lucide-react"
import { Button } from "@/components/ui/button"
interface LoadingButtonProps extends React.ComponentProps<typeof Button> {
loading?: boolean
}
export function LoadingButton({
children,
loading,
disabled,
...props
}: LoadingButtonProps) {
return (
<Button disabled={loading || disabled} {...props}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Button>
)
}
```
## Additional Resources
For detailed patterns, see reference files:
- **`references/components.md`** - Full component catalog
- **`references/theming.md`** - Theme customization
More agent context in azlekov/my-claude-code
6 other files this repository gives its agents.
Skill
- nextjsskills/nextjs/SKILL.md
- postgres-nanoidskills/postgres-nanoid/SKILL.md
- reactskills/react/SKILL.md
- stripeskills/stripe/SKILL.md
- supabase-expertskills/supabase-expert/SKILL.md
- tailwindcssskills/tailwindcss/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool public_context_discussion, action report. How to connect one.

