.claude/skills/shadcn-ui/SKILL.md
Build beautiful, accessible UIs with shadcn/ui components. Use this skill when creating forms, dialogs, tables, sidebars, or any UI components in Next.js. Covers installation, component patterns, react-hook-form integration with Zod validation, and dark mode setup.
npx skillsauth add anasahmed07/doit 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.
Build beautiful, accessible UIs with copy-paste components. shadcn/ui provides a collection of reusable components built with Radix UI and Tailwind CSS.
# Initialize shadcn/ui in your Next.js project
npx shadcn@latest init
# Add components as needed
npx shadcn@latest add button
npx shadcn@latest add form
npx shadcn@latest add dialog
npx shadcn@latest add table
npx shadcn@latest add sidebar
# Create Next.js project with shadcn/ui
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app
# Initialize shadcn/ui
npx shadcn@latest init
# Common components for TaskFlow-style apps
npx shadcn@latest add button card form input label dialog \
table badge sidebar dropdown-menu avatar separator \
select textarea tabs toast sonner
import { Button } from "@/components/ui/button"
// Variants
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Plus /></Button>
// With loading state
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Loading...
</Button>
// As child (for Next.js Link)
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
"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 {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
// Define schema
const taskSchema = z.object({
title: z.string().min(1, "Title is required").max(200),
description: z.string().optional(),
priority: z.enum(["low", "medium", "high", "critical"]),
assignee: z.string().optional(),
})
type TaskFormValues = z.infer<typeof taskSchema>
export function TaskForm({ onSubmit }: { onSubmit: (data: TaskFormValues) => void }) {
const form = useForm<TaskFormValues>({
resolver: zodResolver(taskSchema),
defaultValues: {
title: "",
description: "",
priority: "medium",
},
})
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Task title..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Describe the task..."
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
Optional details about the task.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select priority" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Creating..." : "Create Task"}
</Button>
</form>
</Form>
)
}
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
// Basic Dialog
<Dialog>
<DialogTrigger asChild>
<Button>Create Task</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Create New Task</DialogTitle>
<DialogDescription>
Add a new task to your project. Click save when done.
</DialogDescription>
</DialogHeader>
<TaskForm onSubmit={handleSubmit} />
</DialogContent>
</Dialog>
// Controlled Dialog
const [open, setOpen] = useState(false)
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Edit</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Task</DialogTitle>
</DialogHeader>
<TaskForm
onSubmit={(data) => {
handleUpdate(data)
setOpen(false)
}}
/>
</DialogContent>
</Dialog>
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete Task</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the task
and remove all associated data.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
"use client"
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
getPaginationRowModel,
getSortedRowModel,
SortingState,
} from "@tanstack/react-table"
import { useState } from "react"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
// Define columns
const columns: ColumnDef<Task>[] = [
{
accessorKey: "title",
header: "Title",
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.getValue("status") as string
return (
<Badge variant={status === "completed" ? "default" : "secondary"}>
{status}
</Badge>
)
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string
const colors = {
low: "bg-gray-100 text-gray-800",
medium: "bg-blue-100 text-blue-800",
high: "bg-orange-100 text-orange-800",
critical: "bg-red-100 text-red-800",
}
return (
<Badge className={colors[priority as keyof typeof colors]}>
{priority}
</Badge>
)
},
},
{
accessorKey: "assignee",
header: "Assignee",
},
{
id: "actions",
cell: ({ row }) => {
const task = row.original
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEdit(task)}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(task.id)}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
// DataTable component
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
state: { sorting },
})
return (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}
import { cookies } from "next/headers"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar"
import {
LayoutDashboard,
ListTodo,
Users,
Settings,
Bot,
} from "lucide-react"
// Menu items
const menuItems = [
{ title: "Dashboard", url: "/dashboard", icon: LayoutDashboard },
{ title: "Tasks", url: "/tasks", icon: ListTodo, badge: "12" },
{ title: "Workers", url: "/workers", icon: Users },
{ title: "Agents", url: "/agents", icon: Bot },
{ title: "Settings", url: "/settings", icon: Settings },
]
// AppSidebar component
export function AppSidebar() {
return (
<Sidebar>
<SidebarHeader>
<div className="flex items-center gap-2 px-4 py-2">
<Bot className="h-6 w-6" />
<span className="font-semibold">TaskFlow</span>
</div>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{menuItems.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
{item.badge && (
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
)}
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<UserMenu />
</SidebarFooter>
</Sidebar>
)
}
// Layout with persistent sidebar state
export async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const cookieStore = await cookies()
const defaultOpen = cookieStore.get("sidebar_state")?.value === "true"
return (
<SidebarProvider defaultOpen={defaultOpen}>
<AppSidebar />
<main className="flex-1">
<header className="flex h-14 items-center gap-4 border-b px-4">
<SidebarTrigger />
<h1 className="text-lg font-semibold">Dashboard</h1>
</header>
<div className="p-4">{children}</div>
</main>
</SidebarProvider>
)
}
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Progress } from "@/components/ui/progress"
// Task Card
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">{task.title}</CardTitle>
<Badge variant={task.status === "completed" ? "default" : "secondary"}>
{task.status}
</Badge>
</div>
<CardDescription>
Assigned to {task.assignee}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{task.description}</p>
<div className="mt-4">
<div className="flex justify-between text-sm mb-1">
<span>Progress</span>
<span>{task.progress}%</span>
</div>
<Progress value={task.progress} />
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" size="sm">View Details</Button>
<Button size="sm">Start</Button>
</CardFooter>
</Card>
// Install sonner
// npx shadcn@latest add sonner
// Add Toaster to layout
import { Toaster } from "@/components/ui/sonner"
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Toaster />
</body>
</html>
)
}
// Use toast in components
import { toast } from "sonner"
// Success
toast.success("Task created successfully")
// Error
toast.error("Failed to create task")
// With description
toast("Task Updated", {
description: "The task status has been changed to 'in progress'",
})
// With action
toast("Task assigned", {
description: "Task #123 assigned to @claude-code",
action: {
label: "Undo",
onClick: () => handleUndo(),
},
})
// Promise toast
toast.promise(createTask(data), {
loading: "Creating task...",
success: "Task created!",
error: "Failed to create task",
})
// Install next-themes
// npm install next-themes
// Create theme provider
// 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>
}
// Add to layout
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 component
"use client"
import { useTheme } from "next-themes"
import { Moon, Sun } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
# Core (installed with shadcn init)
# - tailwindcss
# - @radix-ui/* (per component)
# - class-variance-authority
# - clsx
# - tailwind-merge
# - lucide-react
# Forms
npm install react-hook-form @hookform/resolvers zod
# Data Tables
npm install @tanstack/react-table
# Toast
# Added via: npx shadcn@latest add sonner
# Dark Mode
npm install next-themes
components/
├── ui/ # shadcn components (auto-generated)
│ ├── button.tsx
│ ├── card.tsx
│ ├── dialog.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── select.tsx
│ ├── sidebar.tsx
│ ├── table.tsx
│ └── ...
├── forms/ # Custom form components
│ ├── task-form.tsx
│ └── worker-form.tsx
├── tables/ # Custom table components
│ ├── task-table.tsx
│ └── columns.tsx
├── theme-provider.tsx # Dark mode provider
└── app-sidebar.tsx # Custom sidebar
import { Skeleton } from "@/components/ui/skeleton"
// Card skeleton
<Card>
<CardHeader>
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</CardHeader>
<CardContent>
<Skeleton className="h-[125px] w-full rounded-xl" />
</CardContent>
</Card>
// Table skeleton
{Array.from({ length: 5 }).map((_, i) => (
<TableRow key={i}>
<TableCell><Skeleton className="h-4 w-[200px]" /></TableCell>
<TableCell><Skeleton className="h-4 w-[100px]" /></TableCell>
<TableCell><Skeleton className="h-4 w-[80px]" /></TableCell>
</TableRow>
))}
import { FileQuestion } from "lucide-react"
<div className="flex flex-col items-center justify-center py-12">
<FileQuestion className="h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">No tasks found</h3>
<p className="mt-2 text-sm text-muted-foreground">
Get started by creating a new task.
</p>
<Button className="mt-4">
<Plus className="mr-2 h-4 w-4" />
Create Task
</Button>
</div>
For additional documentation, use Context7 MCP:
mcp__context7__get-library-docs with context7CompatibleLibraryID="/shadcn-ui/ui" and topic="form"
Official docs: https://ui.shadcn.com
development
Use when building real-time communication systems with WebSockets or Socket.IO. Invoke for bidirectional messaging, horizontal scaling with Redis, presence tracking, room management.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
development
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
tools
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.