skills/catalog/frontend/shadcn-ui/SKILL.md
Use when setting up or using shadcn/ui components (install, configure, themes, forms, dialogs, tables). Not for non-React UI frameworks.
npx skillsauth add erikstmartin/dotfiles shadcn-uiInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
shadcn/ui copies component source into your project — you own and edit the code directly. Components are built on Radix UI primitives + Tailwind CSS.
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npx shadcn@latest init
The init prompt configures: style (Default/New York), base color, CSS variables vs Tailwind classes, and component path (src/components/ui/).
# Prereqs: Tailwind already configured
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge lucide-react
npx shadcn@latest init
After init, verify components.json exists and globals.css has the CSS variable block:
@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%;
--radius: 0.5rem;
/* ... other vars added by init */
}
.dark { /* dark mode overrides */ }
}
Verify tailwind.config.js has darkMode: ["class"] and plugins: [require("tailwindcss-animate")].
Verify tsconfig.json has path alias:
{ "paths": { "@/*": ["./src/*"] } }
npx shadcn@latest add button
npx shadcn@latest add button input form card dialog select # multiple at once
This copies files into src/components/ui/ and installs any missing @radix-ui/* deps.
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
Some components (Toast, Tooltip) require a provider. Add once to app/layout.tsx:
import { Toaster } from "@/components/ui/toaster"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="min-h-screen bg-background font-sans antialiased">
{children}
<Toaster />
</body>
</html>
)
}
Then trigger toasts from any client component:
"use client"
import { useToast } from "@/components/ui/use-toast"
const { toast } = useToast()
toast({ title: "Saved", description: "Changes persisted." })
toast({ variant: "destructive", title: "Error", description: "Something went wrong." })
Edit CSS variables in globals.css — changes apply to all components automatically:
:root {
--primary: 262 83% 58%; /* purple instead of default */
--primary-foreground: 0 0% 100%;
--radius: 0.75rem; /* rounder corners globally */
}
Use ui.shadcn.com/themes to generate a full variable set.
Open the copied file directly (e.g., src/components/ui/button.tsx) and extend the cva config:
const buttonVariants = cva("inline-flex items-center ...", {
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
// Add your variant:
brand: "bg-gradient-to-r from-violet-500 to-fuchsia-500 text-white hover:opacity-90",
},
},
})
Use it: <Button variant="brand">Upgrade</Button>
Pass className — it merges via cn() (tailwind-merge), so your classes win over defaults:
<Button className="w-full rounded-full text-lg h-14">Full-width pill</Button>
Install deps: npx shadcn@latest add form input button card
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Min 8 characters"),
})
export function LoginForm() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "" },
})
function onSubmit(values: z.infer<typeof schema>) {
console.log(values) // replace with your auth call
}
return (
<Card className="w-[380px]">
<CardHeader><CardTitle>Sign in</CardTitle></CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl><Input type="email" placeholder="[email protected]" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="password" render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl><Input type="password" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<Button type="submit" className="w-full">Sign in</Button>
</form>
</Form>
</CardContent>
</Card>
)
}
Key pattern: FormField wraps each input, FormMessage renders Zod errors automatically, spread {...field} onto the input.
Install: npx shadcn@latest add dialog button
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"
export function DeleteConfirm({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete item</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={() => { onConfirm(); setOpen(false) }}>Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
Key pattern: DialogTrigger asChild passes the trigger role to your button without extra DOM nodes. Control open state manually when you need programmatic close.
Missing "use client" — Radix-based components use hooks and event handlers; they fail in RSC without the directive. Add "use client" to any file that imports interactive shadcn components.
cn() not found — Init generates src/lib/utils.ts with the cn helper. If you moved files, update the import path in each component file.
Form inputs not updating — Always spread {...field} from render={({ field }) => ...}. Omitting it breaks React Hook Form's registration.
Select value not binding in forms — Use onValueChange={field.onChange} (not onChange) and defaultValue={field.value} on the <Select> element, not <SelectTrigger>.
Dark mode not working — tailwind.config.js must have darkMode: ["class"]. Toggle by adding/removing the dark class on <html>.
Stale component code after shadcn update — Components are snapshots; re-run npx shadcn@latest add <component> to get updates, but this overwrites customizations. Diff before overwriting.
testing
Use when creating new skills, editing existing skills, or verifying skills work before deployment
development
Use when you have a spec or requirements for a multi-step task, before touching code
data-ai
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
tools
Use when starting any conversation - establishes how to find and use skills, requiring Skill tool invocation before ANY response including clarifying questions