shadcn

shadcn/ui is a collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project (not installed as a dependency), giving you full control.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "shadcn" with this command: npx skills add justinlevinedotme/jalco-opencode/justinlevinedotme-jalco-opencode-shadcn

shadcn/ui is a collection of reusable components built with Radix UI and Tailwind CSS. Components are copied into your project (not installed as a dependency), giving you full control.

Installation

Initialize in Project

Next.js or Vite

npx shadcn@latest init

Prompts:

  • Style: Default or New York

  • Base color: Slate, Gray, Zinc, Neutral, Stone

  • CSS variables: Yes (recommended)

  • tailwind.config location

  • components.json location

Add Components

Single component

npx shadcn@latest add button

Multiple components

npx shadcn@latest add button card dialog

All components

npx shadcn@latest add --all

With overwrite

npx shadcn@latest add button --overwrite

Project Structure

src/ ├── components/ │ └── ui/ # shadcn components live here │ ├── button.tsx │ ├── card.tsx │ └── dialog.tsx ├── lib/ │ └── utils.ts # cn() helper └── app/ └── globals.css # CSS variables

Core Utilities

cn() Helper

Merges Tailwind classes with conflict resolution:

// lib/utils.ts import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }

Usage:

import { cn } from "@/lib/utils"

<div className={cn( "base-classes", conditional && "conditional-classes", className // Allow override from props )} />

Class Variance Authority (cva)

MUST use for component variants:

import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva( // Base classes "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input bg-background hover:bg-accent", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, size: { default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "default", size: "default", }, } )

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}

const Button = ({ className, variant, size, ...props }: ButtonProps) => ( <button className={cn(buttonVariants({ variant, size, className }))} {...props} /> )

Component Patterns

Button

npx shadcn@latest add button

import { Button } from "@/components/ui/button"

// Variants <Button>Default</Button> <Button variant="secondary">Secondary</Button> <Button variant="destructive">Destructive</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="link">Link</Button>

// Sizes <Button size="sm">Small</Button> <Button size="lg">Large</Button> <Button size="icon"><IconComponent /></Button>

// States <Button disabled>Disabled</Button> <Button asChild> <a href="/link">As Link</a> </Button>

// With loading <Button disabled> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Loading </Button>

Dialog

npx shadcn@latest add dialog

import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, DialogClose, } from "@/components/ui/dialog"

<Dialog> <DialogTrigger asChild> <Button>Open Dialog</Button> </DialogTrigger> <DialogContent className="sm:max-w-[425px]"> <DialogHeader> <DialogTitle>Edit Profile</DialogTitle> <DialogDescription> Make changes to your profile here. </DialogDescription> </DialogHeader> <div className="grid gap-4 py-4"> {/* Content */} </div> <DialogFooter> <DialogClose asChild> <Button variant="outline">Cancel</Button> </DialogClose> <Button type="submit">Save</Button> </DialogFooter> </DialogContent> </Dialog>

Controlled Dialog:

const [open, setOpen] = useState(false)

<Dialog open={open} onOpenChange={setOpen}> <DialogContent> {/* ... */} <Button onClick={() => setOpen(false)}>Close</Button> </DialogContent> </Dialog>

Sheet (Slide-out Panel)

npx shadcn@latest add sheet

import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"

<Sheet> <SheetTrigger asChild> <Button variant="outline">Open</Button> </SheetTrigger> <SheetContent side="right"> {/* left, right, top, bottom /} <SheetHeader> <SheetTitle>Menu</SheetTitle> <SheetDescription>Navigation</SheetDescription> </SheetHeader> {/ Content */} </SheetContent> </Sheet>

Dropdown Menu

npx shadcn@latest add dropdown-menu

import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from "@/components/ui/dropdown-menu"

<DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline">Open</Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-56"> <DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem> Profile <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut> </DropdownMenuItem> <DropdownMenuItem disabled>Billing</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuSub> <DropdownMenuSubTrigger>More</DropdownMenuSubTrigger> <DropdownMenuSubContent> <DropdownMenuItem>Sub Item</DropdownMenuItem> </DropdownMenuSubContent> </DropdownMenuSub> <DropdownMenuSeparator /> <DropdownMenuItem className="text-destructive"> Log out </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu>

Command (Command Palette)

npx shadcn@latest add command dialog

import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, } from "@/components/ui/command"

// As dialog (⌘K pattern) const [open, setOpen] = useState(false)

useEffect(() => { const down = (e: KeyboardEvent) => { if (e.key === "k" && (e.metaKey || e.ctrlKey)) { e.preventDefault() setOpen((open) => !open) } } document.addEventListener("keydown", down) return () => document.removeEventListener("keydown", down) }, [])

<CommandDialog open={open} onOpenChange={setOpen}> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Suggestions"> <CommandItem onSelect={() => { /* action */ }}> <Calendar className="mr-2 h-4 w-4" /> <span>Calendar</span> </CommandItem> <CommandItem> <Search className="mr-2 h-4 w-4" /> <span>Search</span> <CommandShortcut>⌘K</CommandShortcut> </CommandItem> </CommandGroup> <CommandSeparator /> <CommandGroup heading="Settings"> <CommandItem>Profile</CommandItem> <CommandItem>Settings</CommandItem> </CommandGroup> </CommandList> </CommandDialog>

Form (with react-hook-form + zod)

npx shadcn@latest add form input button npm install zod react-hook-form @hookform/resolvers

import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" 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-8"> <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> ) }

Select

npx shadcn@latest add select

import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"

<Select onValueChange={(value) => console.log(value)}> <SelectTrigger className="w-[180px]"> <SelectValue placeholder="Select a fruit" /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectLabel>Fruits</SelectLabel> <SelectItem value="apple">Apple</SelectItem> <SelectItem value="banana">Banana</SelectItem> <SelectItem value="orange" disabled>Orange</SelectItem> </SelectGroup> </SelectContent> </Select>

Card

npx shadcn@latest add card

import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"

<Card className="w-[350px]"> <CardHeader> <CardTitle>Create Project</CardTitle> <CardDescription>Deploy your project in one click.</CardDescription> </CardHeader> <CardContent> {/* Form fields */} </CardContent> <CardFooter className="flex justify-between"> <Button variant="outline">Cancel</Button> <Button>Deploy</Button> </CardFooter> </Card>

Table

npx shadcn@latest add table

import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"

<Table> <TableCaption>A list of recent invoices.</TableCaption> <TableHeader> <TableRow> <TableHead className="w-[100px]">Invoice</TableHead> <TableHead>Status</TableHead> <TableHead className="text-right">Amount</TableHead> </TableRow> </TableHeader> <TableBody> {invoices.map((invoice) => ( <TableRow key={invoice.id}> <TableCell className="font-medium">{invoice.id}</TableCell> <TableCell>{invoice.status}</TableCell> <TableCell className="text-right">{invoice.amount}</TableCell> </TableRow> ))} </TableBody> </Table>

Data Table (with TanStack Table)

npx shadcn@latest add table npm install @tanstack/react-table

// columns.tsx import { ColumnDef } from "@tanstack/react-table"

export type Payment = { id: string amount: number status: "pending" | "processing" | "success" | "failed" email: string }

export const columns: ColumnDef<Payment>[] = [ { accessorKey: "status", header: "Status", }, { accessorKey: "email", header: "Email", }, { accessorKey: "amount", header: () => <div className="text-right">Amount</div>, cell: ({ row }) => { const amount = parseFloat(row.getValue("amount")) const formatted = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", }).format(amount) return <div className="text-right font-medium">{formatted}</div> }, }, ]

// data-table.tsx import { flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table"

export function DataTable<TData, TValue>({ columns, data, }: DataTableProps<TData, TValue>) { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), })

return ( <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> ) }

Toast (Sonner)

npx shadcn@latest add sonner

// app/layout.tsx import { Toaster } from "@/components/ui/sonner"

export default function RootLayout({ children }) { return ( <html> <body> {children} <Toaster /> </body> </html> ) }

// Usage anywhere import { toast } from "sonner"

toast("Event created") toast.success("Success!") toast.error("Error occurred") toast.warning("Warning") toast.info("Info message")

// With description toast("Event created", { description: "Sunday, December 03, 2023 at 9:00 AM", action: { label: "Undo", onClick: () => console.log("Undo"), }, })

// Promise toast toast.promise(saveData(), { loading: "Saving...", success: "Data saved!", error: "Error saving data", })

Tabs

npx shadcn@latest add tabs

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

<Tabs defaultValue="account" className="w-[400px]"> <TabsList className="grid w-full grid-cols-2"> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContent value="account"> <Card> <CardHeader> <CardTitle>Account</CardTitle> </CardHeader> <CardContent>{/* ... /}</CardContent> </Card> </TabsContent> <TabsContent value="password"> <Card> <CardHeader> <CardTitle>Password</CardTitle> </CardHeader> <CardContent>{/ ... */}</CardContent> </Card> </TabsContent> </Tabs>

Theming

CSS Variables

shadcn uses CSS variables for theming in globals.css :

@layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; }

.dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; } }

Dark Mode (next-themes)

npm install next-themes

// app/providers.tsx "use client"

import { ThemeProvider } from "next-themes"

export function Providers({ children }: { children: React.ReactNode }) { return ( <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange > {children} </ThemeProvider> ) }

// app/layout.tsx import { Providers } from "./providers"

export default function RootLayout({ children }) { return ( <html lang="en" suppressHydrationWarning> <body> <Providers>{children}</Providers> </body> </html> ) }

Theme Toggle Component:

"use client"

import { Moon, Sun } from "lucide-react" import { useTheme } from "next-themes" 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="outline" size="icon"> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] 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> ) }

Custom Colors

Add custom colors to your theme:

:root { --success: 142 76% 36%; --success-foreground: 0 0% 100%; --warning: 38 92% 50%; --warning-foreground: 0 0% 100%; }

Use in components:

const alertVariants = cva("...", { variants: { variant: { success: "bg-success text-success-foreground", warning: "bg-warning text-warning-foreground", }, }, })

Common Patterns

Loading Button

import { Loader2 } from "lucide-react"

interface LoadingButtonProps extends ButtonProps { loading?: boolean }

export function LoadingButton({ loading, children, disabled, ...props }: LoadingButtonProps) { return ( <Button disabled={loading || disabled} {...props}> {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {children} </Button> ) }

Confirmation Dialog

interface ConfirmDialogProps { open: boolean onOpenChange: (open: boolean) => void onConfirm: () => void title: string description: string confirmText?: string cancelText?: string destructive?: boolean }

export function ConfirmDialog({ open, onOpenChange, onConfirm, title, description, confirmText = "Confirm", cancelText = "Cancel", destructive = false, }: ConfirmDialogProps) { return ( <AlertDialog open={open} onOpenChange={onOpenChange}> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>{title}</AlertDialogTitle> <AlertDialogDescription>{description}</AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>{cancelText}</AlertDialogCancel> <AlertDialogAction onClick={onConfirm} className={cn(destructive && "bg-destructive text-destructive-foreground hover:bg-destructive/90")} > {confirmText} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ) }

Responsive Drawer/Dialog

Mobile drawer, desktop dialog:

npx shadcn@latest add dialog drawer

import { useMediaQuery } from "@/hooks/use-media-query"

export function ResponsiveDialog({ children, ...props }) { const isDesktop = useMediaQuery("(min-width: 768px)")

if (isDesktop) { return <Dialog {...props}>{children}</Dialog> }

return <Drawer {...props}>{children}</Drawer> }

Combobox (Searchable Select)

npx shadcn@latest add command popover

import { Check, ChevronsUpDown } from "lucide-react" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"

const frameworks = [ { value: "next.js", label: "Next.js" }, { value: "sveltekit", label: "SvelteKit" }, { value: "nuxt.js", label: "Nuxt.js" }, ]

export function Combobox() { const [open, setOpen] = useState(false) const [value, setValue] = useState("")

return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between" > {value ? frameworks.find((f) => f.value === value)?.label : "Select framework..."} <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> </Button> </PopoverTrigger> <PopoverContent className="w-[200px] p-0"> <Command> <CommandInput placeholder="Search framework..." /> <CommandList> <CommandEmpty>No framework found.</CommandEmpty> <CommandGroup> {frameworks.map((framework) => ( <CommandItem key={framework.value} value={framework.value} onSelect={(currentValue) => { setValue(currentValue === value ? "" : currentValue) setOpen(false) }} > <Check className={cn( "mr-2 h-4 w-4", value === framework.value ? "opacity-100" : "opacity-0" )} /> {framework.label} </CommandItem> ))} </CommandGroup> </CommandList> </Command> </PopoverContent> </Popover> ) }

Available Components

Layout

  • Accordion, AspectRatio, Card, Collapsible, ResizablePanels, ScrollArea, Separator, Tabs

Forms

  • Button, Checkbox, Form, Input, InputOTP, Label, RadioGroup, Select, Slider, Switch, Textarea, Toggle, ToggleGroup

Data Display

  • Avatar, Badge, Calendar, DataTable, Table

Feedback

  • Alert, AlertDialog, Progress, Skeleton, Sonner (Toast), Tooltip

Navigation

  • Breadcrumb, Command, ContextMenu, DropdownMenu, Menubar, NavigationMenu, Pagination

Overlay

  • Dialog, Drawer, HoverCard, Popover, Sheet

Tips

  • MUST use asChild when wrapping custom elements in triggers

  • MUST use cn() for class merging - never concatenate classes manually

  • Controlled vs Uncontrolled - Use defaultValue for uncontrolled, value

  • onValueChange for controlled
  • Accessibility - Radix handles a11y, but SHOULD always add sr-only labels for icon buttons

  • Customize at source - Edit files in components/ui/ directly for project-wide changes

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

shadcn

Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset".

Repository Source
20K109.7Kshadcn
Coding

shadcn

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

shadcn

No summary provided by upstream source.

Repository SourceNeeds Review