shadcn-ui
Skill1 starsChanged 10 months agoApache-2.0View on GitHub
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.
- Installs packages
Files
shadcn/ui
Build beautiful, accessible UIs with copy-paste components. shadcn/ui provides a collection of reusable components built with Radix UI and Tailwind CSS.
When to Use
- Building UI components for Next.js applications
- Creating forms with validation (react-hook-form + Zod)
- Implementing dialogs, modals, and alerts
- Building data tables with sorting/filtering
- Creating dashboard layouts with sidebars
- Adding dark mode support
Quick Start
# 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 sidebarCore Patterns
1. Project Setup
# 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 sonner2. Button Variants
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>3. Forms with react-hook-form + Zod
"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>
)
}4. Dialog / Modal
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>5. Alert Dialog (Confirmation)
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>6. Data Table
"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>
)
}7. Sidebar Navigation
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>
)
}8. Card Component
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>9. Toast Notifications (Sonner)
// 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",
})10. Dark Mode
// 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>
)
}Dependencies
# 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-themesFile Structure
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 sidebarCommon Patterns
Loading States
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>
))}Empty States
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>References
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
Other versions41 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 41
- QuantumNous/new-api★ 51ksame name
- BuilderIO/agent-native★ 7.4ksame name
- ExplosiveCoderflome/AI-Novel-Writing-Assistant★ 3.2ksame name
- codedogQBY/ReadAny★ 2.7ksame name
- shep-ai/shep★ 269same name
- diodeme/Gold-Band★ 160same name
- mweinbach/agent-coworker★ 156same name
- jh941213/my-claude-code-asset★ 126same name
- nahid-sparktales/agent-dispatcher★ 59same name
- RootCX/RootCX★ 49same name
31 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
Used alongsideProjects with it also use i18n-translate, api-design-principles and 8 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
lien0219Skilli18n-translate
Complete and maintain frontend i18n translations for this project. Covers finding missing translation keys, detecting untranslated entries, and adding translations for all supported locales (en, zh, fr, ja, ru, vi). Use when the user asks to add translations, fix i18n, complete missing translations, or when new UI text needs to be internationalized.
Also in 4 projects that use this
nilecuiSkillapi-design-principles
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards.
Also in 5 projects that use this
OpenAISkillfigma-create-new-file
Create a new blank Figma file. Use when the user wants to create a new Figma design or FigJam file, or when you need a new file before calling use_figma. Handles plan resolution via whoami if needed. Usage — /figma-create-new-file [editorType] [fileName] (e.g. /figma-create-new-file figjam My Whiteboard)
Also in 4 projects that use this
HK-hubSkillshader-dev
Comprehensive GLSL shader techniques for creating stunning visual effects — ray marching, SDF modeling, fluid simulation, particle systems, procedural generation, lighting, post-processing, and more.
Also in 3 projects that use this
xfstudioSkilltailwind-design-system
Build scalable design systems with Tailwind CSS, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.
Also in 5 projects that use this
OpenAISkillfigma-use
**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug failures. Trigger whenever the user wants to perform a write action or a unique read action that requires JavaScript execution in the Figma file context — e.g. create/edit/delete nodes, set up variables or tokens, build components and variants, modify auto-layout or fills, bind variables to properties, or inspect file structure programmatically.
Also in 4 projects that use this
- +1 more from openai
In this repository26 other files it gives its agents.
- AGENTS.mdAGENTS.md
- backend/AGENTS.mdAGENTS.md
- .claude/skills/vercel-react-best-practices/AGENTS.mdAGENTS.md
- doit-cli/AGENTS.mdAGENTS.md
- frontend/AGENTS.mdAGENTS.md
- backend/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- doit-cli/CLAUDE.mdCLAUDE.md
- frontend/CLAUDE.mdCLAUDE.md
- better-auth-best-practicesSkill
- containerize-appsSkill
- create-auth-skillSkill
- fastapi-backendSkill
- frontend-designSkill
- helm-chartsSkill
- kagentSkill
- kubectl-aiSkill
- kubernetes-deployment-validatorSkill
- kubernetes-essentialsSkill
- mcp-builderSkill
- minikubeSkill
- nextjs-16Skill
- skill-creatorSkill
- vercel-react-best-practicesSkill
- web-design-guidelinesSkill
- websocket-engineerSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

