Loading...
Loading...
Use this skill when building user interfaces in a Next.js project that uses shadcn/ui. Triggers include any request to create, update, or refactor React components, pages, forms, dialogs, tables, or layouts. Also use when the user asks about component installation, styling with Tailwind, form validation, toast notifications, or theming. Use this skill whenever the project stack involves shadcn/ui, Tailwind CSS, React Hook Form, Zod, or Sonner.
npx skill4agent add ridh21/skills shadcn-ui-developmentpnpmnpmyarnpnpm install
pnpm add package-name
pnpm dlx shadcn@latest add button// ✅ CORRECT
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
// ❌ WRONG
import * as Dialog from "@radix-ui/react-dialog"
import { Button } from "@mui/material"pnpm dlx shadcn@latest add [component-name]// ✅ CORRECT
<div className="flex items-center gap-4 p-6 rounded-lg border bg-card">
// ❌ WRONG
<div style={{ display: 'flex', padding: '24px' }}>bg-background text-foreground
bg-primary text-primary-foreground
bg-secondary text-secondary-foreground
bg-muted text-muted-foreground
bg-card text-card-foreground
bg-destructive text-destructive-foreground
border-border border-inputcn()@/lib/utilsreact-hot-toastimport { toast } from "sonner"
toast.success("Saved successfully!")
toast.error("Something went wrong")
toast.loading("Saving...")
toast.promise(saveData(), {
loading: "Saving...",
success: "Saved!",
error: "Failed to save"
})<Toaster />@/components/ui/sonnerimport { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const schema = z.object({ email: z.string().email() })
export function MyForm() {
const form = useForm({ resolver: zodResolver(schema) })
return (
<Form {...form}>
<form onSubmit={form.handleSubmit((v) => console.log(v))} className="space-y-6">
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<Button type="submit">Submit</Button>
</form>
</Form>
)
}// app/layout.tsx
import { DM_Sans } from "next/font/google"
const dmSans = DM_Sans({ subsets: ["latin"], weight: ["400","500","600","700"], variable: "--font-dm-sans" })
// tailwind.config.ts
fontFamily: { sans: ["var(--font-dm-sans)", "system-ui", "sans-serif"] }<h1 className="text-4xl font-bold tracking-tight">Heading</h1>
<h2 className="text-2xl font-semibold">Section</h2>
<p className="text-base text-muted-foreground">Body</p>
<span className="text-xs font-medium uppercase tracking-wide">Label</span>oklch(L C H)
/* L = lightness 0–1 (0 = black, 1 = white) */
/* C = chroma 0–0.4 (0 = gray, higher = more saturated) */
/* H = hue 0–360 (degrees on color wheel) */@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
/* Neutral base */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
/* Primary accent — change H to shift the hue */
--primary: oklch(0.55 0.22 29); /* e.g. vivid red-orange */
--primary-foreground: oklch(0.985 0.01 29);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.3);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.55 0.22 29);
--radius: 0.5rem;
/* Charts */
--chart-1: oklch(0.646 0.222 41.1);
--chart-2: oklch(0.6 0.118 184.7);
--chart-3: oklch(0.398 0.07 227.4);
--chart-4: oklch(0.828 0.189 84.4);
--chart-5: oklch(0.769 0.188 70.1);
/* Sidebar */
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.55 0.22 29);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.55 0.22 29);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.55 0.22 29);
--primary-foreground: oklch(0.985 0.01 29);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.7);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.55 0.22 29);
/* Charts — dark */
--chart-1: oklch(0.488 0.243 264.4);
--chart-2: oklch(0.696 0.17 162.5);
--chart-3: oklch(0.769 0.188 70.1);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.4);
/* Sidebar — dark */
--sidebar: oklch(0.145 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.55 0.22 29);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.55 0.22 29);
}
@layer base {
* { @apply border-border; }
body { @apply bg-background text-foreground antialiased; }
html { scroll-behavior: smooth; }
:focus-visible { @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-background; }
}--primary--primary-foreground--ring| Color | H value | Example OKLCH |
|---|---|---|
| Red | 29 | |
| Orange | 60 | |
| Yellow | 90 | |
| Green | 145 | |
| Teal | 185 | |
| Blue | 250 | |
| Violet | 290 | |
| Pink | 340 | |
/* ❌ WRONG */
--primary: #e53e3e;
--primary: rgb(229, 62, 62);
--primary: hsl(0, 72%, 50%);
/* ✅ CORRECT */
--primary: oklch(0.55 0.22 29);shadow-smshadow-mdtransition-all duration-200<Skeleton>toast.loading()<AlertDialog>sm:md:lg:import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
export function DataTable({ data }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell><Badge>{item.status}</Badge></TableCell>
<TableCell><Button variant="ghost" size="sm">Edit</Button></TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export function CreateDialog() {
return (
<Dialog>
<DialogTrigger asChild><Button>Create New</Button></DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Create Item</DialogTitle></DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input id="name" placeholder="Enter name" />
</div>
</div>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}import { Skeleton } from "@/components/ui/skeleton"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
export function LoadingCard() {
return (
<Card>
<CardHeader><Skeleton className="h-4 w-[250px]" /></CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-[200px]" />
</CardContent>
</Card>
)
}| ❌ Never Do | ✅ Do Instead |
|---|---|
| |
| Use Tailwind classes |
| |
| |
| |
| |