Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 109% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 42% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 88% | 0% |
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.
| 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 |
bashnpx shadcn@latest init
Configuration prompts:
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
Merge Tailwind classes conditionally:
tsximport { 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} /> ) }
tsximport { 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>
tsximport { 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>
tsximport { 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>
tsximport { 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>
tsximport { 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>
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> ) }
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> ) }
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> ) }
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> ) }
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" })
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> ) }
tsximport { 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> ) }
For detailed patterns, see reference files:
references/components.md - Full component catalogreferences/theming.md - Theme customizationOther measured skills in the registry, with their headline benchmark lift.