shadcn-ui

shadcn/ui is the default component and styling approach for all frontend projects. Always prefer shadcn/ui components (<Button> , <Input> , <Card> , etc.) over raw HTML elements or hand-styled Tailwind.

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-ui" with this command: npx skills add michaelkeevildown/claude-agents-skills/michaelkeevildown-claude-agents-skills-shadcn-ui

shadcn/ui

When to Use

shadcn/ui is the default component and styling approach for all frontend projects. Always prefer shadcn/ui components (<Button> , <Input> , <Card> , etc.) over raw HTML elements or hand-styled Tailwind.

Use this skill for:

  • Component installation, composition, and customization

  • Theming with CSS variables (OKLCH color space)

  • Form integration with react-hook-form + zod

  • Accessible UI patterns (via Radix UI primitives)

Only skip shadcn/ui when a project explicitly uses a different component system (e.g., MUI, Chakra).

Installation and Setup

Initialize in a Next.js Project

npx shadcn@latest init

This creates:

  • components.json — configuration file (style is always "new-york" , the only supported style)

  • lib/utils.ts — the cn() utility (clsx + tailwind-merge)

  • components/ui/ — raw shadcn/ui components (avoid heavy modifications)

  • Recommended additional directories: components/primitives/ (lightly customized wrappers), components/blocks/ (product-level compositions)

Key components.json fields:

  • tailwind.config : Leave blank for Tailwind v4 projects

  • tailwind.cssVariables : true (default, uses OKLCH color space)

  • registries : Optional array for custom component registries

CLI Commands

npx shadcn@latest init # Initialize project npx shadcn@latest add button # Add component(s) npx shadcn@latest add --all # Add all components npx shadcn@latest list # List available components npx shadcn@latest diff button # Show changes vs registry npx shadcn@latest build # Build registry for publishing npx shadcn@latest migrate radix # Migrate to unified radix-ui package

Use --rtl flag with init for right-to-left layout support.

Components are copied into your codebase (not installed as dependencies). You own the code and can modify it.

Ecosystem Notes

  • Tailwind v4: CSS-first configuration via @theme directive in globals.css . No tailwind.config.js needed. Tailwind v3 projects are still supported.

  • tw-animate-css: Replaces deprecated tailwindcss-animate . Install with npm install tw-animate-css and import in globals.css : @import "tw-animate-css";

  • Unified Radix package: The radix-ui package replaces individual @radix-ui/react-* packages. Migrate with npx shadcn@latest migrate radix .

  • React 19: forwardRef is no longer needed. Components accept ref as a regular prop. Both patterns work in existing codebases.

  • "new-york" only: The old "default" style is deprecated. All new projects use "new-york".

The cn() Utility

Always use cn() for conditional/merged class names:

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

<div className={cn( "flex items-center gap-2", isActive && "bg-primary text-primary-foreground", className, // always forward className prop )} />;

Component Usage Patterns

Dialog

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

function DeleteDialog({ onConfirm }: { onConfirm: () => void }) { const [open, setOpen] = useState(false);

return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button variant="destructive">Delete</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> ); }

Sheet (Side Panel)

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

<Sheet> <SheetTrigger asChild> <Button variant="outline">Open Settings</Button> </SheetTrigger> <SheetContent side="right" className="w-[400px]"> <SheetHeader> <SheetTitle>Settings</SheetTitle> <SheetDescription>Configure your preferences.</SheetDescription> </SheetHeader> {/* Content here */} </SheetContent> </Sheet>;

Command (Command Palette / Combobox)

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

function CommandMenu() { 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); }, []);

return ( <CommandDialog open={open} onOpenChange={setOpen}> <CommandInput placeholder="Type a command or search..." /> <CommandList> <CommandEmpty>No results found.</CommandEmpty> <CommandGroup heading="Actions"> <CommandItem onSelect={() => {}}> <span>New Document</span> </CommandItem> </CommandGroup> </CommandList> </CommandDialog> ); }

DataTable (with TanStack Table)

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { ColumnDef, flexRender, getCoreRowModel, useReactTable, } from "@tanstack/react-table";

// Add getSortedRowModel, getFilteredRowModel, getPaginationRowModel as needed

function DataTable<TData, TValue>({ columns, data, }: { columns: ColumnDef<TData, TValue>[]; data: TData[]; }) { 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.map((row) => ( <TableRow key={row.id}> {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </TableCell> ))} </TableRow> ))} </TableBody> </Table> ); }

Other Notable Components

These are available via npx shadcn@latest add <name> (see official docs for usage):

  • Sidebar — app-level navigation with collapsible groups

  • Chart — chart components built on Recharts

  • Drawer — mobile-friendly bottom sheet (Vaul-based)

  • Carousel — content carousel (Embla-based)

  • Resizable — resizable panel layouts

  • Field, Input Group, Button Group — form layout helpers

  • Spinner — loading spinner component

  • Kbd — keyboard shortcut display

  • Empty — empty state component

Composition Patterns

Controlled vs Uncontrolled

Most shadcn/ui components support both patterns:

// Uncontrolled — component manages its own state <Dialog> <DialogTrigger>Open</DialogTrigger> <DialogContent>...</DialogContent> </Dialog>

// Controlled — you manage the state const [open, setOpen] = useState(false) <Dialog open={open} onOpenChange={setOpen}> <DialogContent>...</DialogContent> </Dialog>

Use controlled when you need to:

  • Close on form submit

  • Open programmatically (e.g., after an action)

  • Prevent closing under certain conditions

The asChild Pattern

Use asChild to render a different element while keeping the trigger behavior:

// Renders a <button> wrapping an <a> — BAD <DialogTrigger> <Link href="/settings">Settings</Link> </DialogTrigger>

// Renders just the <a> with trigger behavior — GOOD <DialogTrigger asChild> <Link href="/settings">Settings</Link> </DialogTrigger>

Forwarding className

Always forward className in custom components built on shadcn/ui:

interface CustomCardProps extends React.ComponentProps<typeof Card> { title: string; }

function CustomCard({ title, className, ...props }: CustomCardProps) { return ( <Card className={cn("p-6", className)} {...props}> <CardTitle>{title}</CardTitle> </Card> ); }

With React 19, ref is a regular prop — no forwardRef wrapper needed. React.ComponentProps<typeof Card> already includes ref in React 19.

Styling and Theming

CSS Variables

shadcn/ui uses CSS variables for theming, defined in globals.css . New projects use OKLCH color space. In Tailwind v4 projects, theme tokens are registered with @theme inline instead of tailwind.config.js . Existing HSL-based projects continue to work.

:root { --background: oklch(1 0 0); --foreground: oklch(0.141 0.005 285.823); --primary: oklch(0.21 0.006 285.885); --primary-foreground: oklch(0.985 0.002 247.858); --secondary: oklch(0.967 0.001 286.375); --secondary-foreground: oklch(0.21 0.006 285.885); --muted: oklch(0.967 0.001 286.375); --muted-foreground: oklch(0.552 0.016 285.938); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.92 0.004 286.32); --input: oklch(0.92 0.004 286.32); --ring: oklch(0.705 0.015 286.067); --radius: 0.625rem; }

.dark { --background: oklch(0.141 0.005 285.823); --foreground: oklch(0.985 0.002 247.858); /* ... dark mode values */ }

Extending with Tailwind

Use Tailwind utilities to customize components. The cn() function handles merge conflicts:

<Button className="w-full justify-start text-left font-normal"> Select a date </Button>

Dark Mode

shadcn/ui supports dark mode via the dark class on <html> . Use next-themes :

import { ThemeProvider } from "next-themes";

// In layout.tsx <ThemeProvider attribute="class" defaultTheme="system" enableSystem> {children} </ThemeProvider>;

Toggle:

import { useTheme } from "next-themes";

function ThemeToggle() { const { setTheme, theme } = useTheme(); return ( <Button variant="ghost" size="icon" onClick={() => setTheme(theme === "dark" ? "light" : "dark")} > <Sun className="h-5 w-5 rotate-0 scale-100 dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-5 w-5 rotate-90 scale-0 dark:rotate-0 dark:scale-100" /> </Button> ); }

Form Integration (react-hook-form + zod)

Schema Definition

import { z } from "zod";

const formSchema = z.object({ name: z.string().min(2, "Name must be at least 2 characters"), email: z.string().email("Invalid email address"), role: z.enum(["admin", "user", "viewer"]), notifications: z.boolean().default(false), });

type FormValues = z.infer<typeof formSchema>;

Form Component

import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form";

function UserForm() { const form = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { name: "", email: "", role: "user", notifications: false, }, });

function onSubmit(values: FormValues) { // values is fully typed and validated console.log(values); }

return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input placeholder="John Doe" {...field} /> </FormControl> <FormDescription>Your display name.</FormDescription> <FormMessage /> </FormItem> )} />

    &#x3C;FormField
      control={form.control}
      name="email"
      render={({ field }) => (
        &#x3C;FormItem>
          &#x3C;FormLabel>Email&#x3C;/FormLabel>
          &#x3C;FormControl>
            &#x3C;Input type="email" {...field} />
          &#x3C;/FormControl>
          &#x3C;FormMessage />
        &#x3C;/FormItem>
      )}
    />

    &#x3C;FormField
      control={form.control}
      name="role"
      render={({ field }) => (
        &#x3C;FormItem>
          &#x3C;FormLabel>Role&#x3C;/FormLabel>
          &#x3C;Select onValueChange={field.onChange} defaultValue={field.value}>
            &#x3C;FormControl>
              &#x3C;SelectTrigger>
                &#x3C;SelectValue placeholder="Select a role" />
              &#x3C;/SelectTrigger>
            &#x3C;/FormControl>
            &#x3C;SelectContent>
              &#x3C;SelectItem value="admin">Admin&#x3C;/SelectItem>
              &#x3C;SelectItem value="user">User&#x3C;/SelectItem>
              &#x3C;SelectItem value="viewer">Viewer&#x3C;/SelectItem>
            &#x3C;/SelectContent>
          &#x3C;/Select>
          &#x3C;FormMessage />
        &#x3C;/FormItem>
      )}
    />

    &#x3C;Button type="submit">Save&#x3C;/Button>
  &#x3C;/form>
&#x3C;/Form>

); }

Accessibility

Built-in Defaults

shadcn/ui components (via Radix UI) include:

  • Keyboard navigation (Tab, Enter, Escape, Arrow keys)

  • Focus management (focus trap in dialogs, return focus on close)

  • ARIA attributes (role, aria-expanded, aria-controls, etc.)

  • Screen reader announcements

Required Additions

You must still provide:

// 1. Always include DialogDescription (or visually hide it) <DialogHeader> <DialogTitle>Edit Profile</DialogTitle> <DialogDescription>Make changes to your profile here.</DialogDescription> </DialogHeader>

// If no visible description, use VisuallyHidden: <DialogDescription className="sr-only"> Dialog for editing profile settings </DialogDescription>

// 2. Label form inputs <FormField name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> {/* Required for accessibility */} <FormControl> <Input {...field} /> </FormControl> </FormItem> )} />

// 3. Add aria-label for icon-only buttons <Button variant="ghost" size="icon" aria-label="Close menu"> <X className="h-4 w-4" /> </Button>

// 4. data-testid for testing <Button data-testid="submit-form">Submit</Button>

Common UI Patterns

Loading States

import { Loader2 } from "lucide-react";

<Button disabled={isLoading}> {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {isLoading ? "Saving..." : "Save Changes"} </Button>;

// Skeleton for content loading import { Skeleton } from "@/components/ui/skeleton";

<Card> <CardHeader> <Skeleton className="h-6 w-[200px]" /> <Skeleton className="h-4 w-[300px]" /> </CardHeader> <CardContent> <Skeleton className="h-20 w-full" /> </CardContent> </Card>;

Empty States

Use the <Empty> component from shadcn/ui, or build a simple one:

<div className="flex flex-col items-center justify-center py-12 text-center"> <InboxIcon className="h-12 w-12 text-muted-foreground" /> <h3 className="mt-4 text-lg font-semibold">No results</h3> <p className="mt-2 text-sm text-muted-foreground"> Get started by creating your first item. </p> <Button className="mt-4">Create New</Button> </div>

Error Display

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertCircle } from "lucide-react";

function ErrorAlert({ message }: { message: string }) { return ( <Alert variant="destructive"> <AlertCircle className="h-4 w-4" /> <AlertTitle>Error</AlertTitle> <AlertDescription>{message}</AlertDescription> </Alert> ); }

Toast Notifications (Sonner)

The old useToast hook is deprecated. Use sonner instead:

npx shadcn@latest add sonner

Add <Toaster /> once in your root layout:

import { Toaster } from "@/components/ui/sonner";

export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html> <body> {children} <Toaster /> </body> </html> ); }

import { toast } from "sonner";

function SaveButton() { async function handleSave() { try { await save(); toast.success("Your changes have been saved."); } catch { toast.error("Failed to save changes."); } }

return <Button onClick={handleSave}>Save</Button>; }

Anti-Patterns

  1. Raw HTML Instead of Components

// BAD <button className="bg-blue-500 text-white px-4 py-2 rounded">Click</button>

// GOOD <Button>Click</Button>

  1. Not Using asChild for Custom Triggers

// BAD — nested interactive elements <DialogTrigger><button>Open</button></DialogTrigger>

// GOOD <DialogTrigger asChild><Button>Open</Button></DialogTrigger>

  1. Hardcoding Colors Instead of CSS Variables

// BAD <div className="bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white">

// GOOD — uses theme variables, dark mode automatic <div className="bg-background text-foreground">

Never hardcode oklch(...) values either — always use semantic class names like bg-primary , text-muted-foreground .

  1. Missing Form Validation Feedback

// BAD — no error message shown <FormField name="email" render={({ field }) => ( <FormItem> <Input {...field} /> </FormItem> )} />

// GOOD — shows validation errors <FormField name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl><Input {...field} /></FormControl> <FormMessage /> {/* Shows zod validation errors */} </FormItem> )} />

  1. Not Forwarding Props

// BAD — className, onClick, etc. are lost function MyButton({ label }: { label: string }) { return <Button>{label}</Button>; }

// GOOD — all button props forwarded function MyButton({ label, ...props }: { label: string } & React.ComponentProps<typeof Button>) { return <Button {...props}>{label}</Button>; }

  1. Using Deprecated useToast

// DEPRECATED — do not use import { useToast } from "@/hooks/use-toast"; const { toast } = useToast();

// CORRECT — use sonner import { toast } from "sonner"; toast.success("Saved");

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.

Automation

neo4j-data-models

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

neo4j-cypher

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

git-workflow

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

neo4j-driver-js

No summary provided by upstream source.

Repository SourceNeeds Review