shadcn

shadcn/ui Development Guidelines

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 squirrelfishcityhall150/claude-code-kit/squirrelfishcityhall150-claude-code-kit-shadcn

shadcn/ui Development Guidelines

Best practices for using shadcn/ui components with Tailwind CSS and Radix UI primitives.

Core Principles

  • Copy, Don't Install: Components are copied to your project, not installed as dependencies

  • Customizable: Modify components directly in your codebase

  • Accessible: Built on Radix UI primitives with ARIA support

  • Type-Safe: Full TypeScript support

  • Composable: Build complex UIs from simple primitives

Installation

Initial Setup

npx shadcn@latest init

Add Components

Add individual components

npx shadcn@latest add button npx shadcn@latest add form npx shadcn@latest add dialog

Add multiple

npx shadcn@latest add button card dialog

Troubleshooting

npm Cache Errors (ENOTEMPTY)

If npx shadcn@latest add fails with npm cache errors like ENOTEMPTY or syscall rename :

Solution 1: Clear npm cache

npm cache clean --force npx shadcn@latest add table

Solution 2: Use pnpm (recommended)

pnpm dlx shadcn@latest add table

Solution 3: Use yarn

yarn dlx shadcn@latest add table

Solution 4: Manual component installation

Visit the shadcn/ui documentation for the specific component and copy the code directly into your project.

Component Usage

Button & Card

import { Button } from '@/components/ui/button'; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';

// Variants <Button>Default</Button> <Button variant="destructive">Destructive</Button> <Button variant="outline">Outline</Button>

// Card <Card> <CardHeader> <CardTitle>{post.title}</CardTitle> <CardDescription>{post.author}</CardDescription> </CardHeader> <CardContent> <p>{post.excerpt}</p> </CardContent> </Card>

Dialog

import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button';

export function CreatePostDialog() { return ( <Dialog> <DialogTrigger asChild> <Button>Create Post</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Create New Post</DialogTitle> <DialogDescription> Fill in the details below to create a new post. </DialogDescription> </DialogHeader> <PostForm /> </DialogContent> </Dialog> ); }

Forms

Basic Form with react-hook-form

'use client';

import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; 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'; import { Textarea } from '@/components/ui/textarea';

const formSchema = z.object({ title: z.string().min(1, 'Title is required'), content: z.string().min(10, 'Content must be at least 10 characters') });

export function PostForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { title: '', content: '' } });

const onSubmit = (values: z.infer<typeof formSchema>) => { console.log(values); };

return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="title" render={({ field }) => ( <FormItem> <FormLabel>Title</FormLabel> <FormControl> <Input placeholder="Post title" {...field} /> </FormControl> <FormMessage /> </FormItem> )} />

    &#x3C;FormField
      control={form.control}
      name="content"
      render={({ field }) => (
        &#x3C;FormItem>
          &#x3C;FormLabel>Content&#x3C;/FormLabel>
          &#x3C;FormControl>
            &#x3C;Textarea placeholder="Write your post..." {...field} />
          &#x3C;/FormControl>
          &#x3C;FormMessage />
        &#x3C;/FormItem>
      )}
    />

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

); }

Select Field

<FormField control={form.control} name="category" render={({ field }) => ( <FormItem> <FormLabel>Category</FormLabel> <Select onValueChange={field.onChange} defaultValue={field.value}> <FormControl> <SelectTrigger> <SelectValue placeholder="Select a category" /> </SelectTrigger> </FormControl> <SelectContent> <SelectItem value="tech">Technology</SelectItem> <SelectItem value="design">Design</SelectItem> <SelectItem value="business">Business</SelectItem> </SelectContent> </Select> <FormMessage /> </FormItem> )} />

Data Display

Table

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

export function PostsTable({ posts }: { posts: Post[] }) { return ( <Table> <TableHeader> <TableRow> <TableHead>Title</TableHead> <TableHead>Author</TableHead> <TableHead>Status</TableHead> <TableHead className="text-right">Actions</TableHead> </TableRow> </TableHeader> <TableBody> {posts.map((post) => ( <TableRow key={post.id}> <TableCell className="font-medium">{post.title}</TableCell> <TableCell>{post.author.name}</TableCell> <TableCell> <Badge variant={post.published ? 'default' : 'secondary'}> {post.published ? 'Published' : 'Draft'} </Badge> </TableCell> <TableCell className="text-right"> <Button variant="ghost" size="sm">Edit</Button> </TableCell> </TableRow> ))} </TableBody> </Table> ); }

Navigation

Badge & Dropdown Menu

import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button';

export function UserMenu() { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost"> <User className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>My Account</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem>Profile</DropdownMenuItem> <DropdownMenuItem>Settings</DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>Log out</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }

Tabs

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

export function PostTabs() { return ( <Tabs defaultValue="published"> <TabsList> <TabsTrigger value="published">Published</TabsTrigger> <TabsTrigger value="drafts">Drafts</TabsTrigger> <TabsTrigger value="archived">Archived</TabsTrigger> </TabsList> <TabsContent value="published"> <PublishedPosts /> </TabsContent> <TabsContent value="drafts"> <DraftPosts /> </TabsContent> <TabsContent value="archived"> <ArchivedPosts /> </TabsContent> </Tabs> ); }

Feedback

Toast

'use client';

import { useToast } from '@/components/ui/use-toast'; import { Button } from '@/components/ui/button';

export function ToastExample() { const { toast } = useToast();

return ( <Button onClick={() => { toast({ title: 'Post created', description: 'Your post has been published successfully.' }); }} > Create Post </Button> ); }

// With variant toast({ variant: 'destructive', title: 'Error', description: 'Failed to create post. Please try again.' });

Alert

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

export function AlertExample() { return ( <Alert variant="destructive"> <AlertCircle className="h-4 w-4" /> <AlertTitle>Error</AlertTitle> <AlertDescription> Your session has expired. Please log in again. </AlertDescription> </Alert> ); }

Loading States

Skeleton

import { Skeleton } from '@/components/ui/skeleton';

export function PostCardSkeleton() { return ( <div className="flex flex-col space-y-3"> <Skeleton className="h-[125px] w-full rounded-xl" /> <div className="space-y-2"> <Skeleton className="h-4 w-[250px]" /> <Skeleton className="h-4 w-[200px]" /> </div> </div> ); }

Customization

Modifying Components

Components are in your codebase - edit them directly:

// components/ui/button.tsx export const buttonVariants = cva( "inline-flex items-center justify-center...", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", // Add custom variant brand: "bg-gradient-to-r from-blue-500 to-purple-600 text-white" } } } );

Using Custom Variant

<Button variant="brand">Custom Brand Button</Button>

Theming

CSS Variables (OKLCH Format)

shadcn/ui now uses OKLCH color format for better color accuracy and perceptual uniformity:

/* app/globals.css / @layer base { :root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0); / ... */ }

.dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --primary: oklch(0.598 0.15 264); --primary-foreground: oklch(0.205 0 0); /* ... */ } }

Dark Mode

// components/theme-toggle.tsx 'use client';

import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; import { Button } from '@/components/ui/button';

export function ThemeToggle() { const { setTheme, theme } = useTheme();

return ( <Button variant="ghost" size="icon" onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')} > <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <span className="sr-only">Toggle theme</span> </Button> ); }

Composition Patterns

Combining Components

export function CreatePostCard() { return ( <Card> <CardHeader> <CardTitle>Create Post</CardTitle> <CardDescription>Share your thoughts with the world</CardDescription> </CardHeader> <CardContent> <PostForm /> </CardContent> <CardFooter className="flex justify-between"> <Button variant="outline">Save Draft</Button> <Button>Publish</Button> </CardFooter> </Card> ); }

Modal with Form

export function CreatePostModal() { const [open, setOpen] = useState(false);

return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button>New Post</Button> </DialogTrigger> <DialogContent className="sm:max-w-[600px]"> <DialogHeader> <DialogTitle>Create Post</DialogTitle> </DialogHeader> <PostForm onSuccess={() => setOpen(false)} /> </DialogContent> </Dialog> ); }

Additional Resources

For detailed information, see:

  • Component Catalog

  • Form Patterns

  • Theming Guide

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

No summary provided by upstream source.

Repository SourceNeeds Review
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
19.8K109.7Kshadcn