shadcn
Scannednpx machina-cli add skill Squirrelfishcityhall150/claude-code-kit/shadcn --openclawshadcn/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>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea placeholder="Write your post..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Create Post</Button>
</form>
</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:
Source
git clone https://github.com/Squirrelfishcityhall150/claude-code-kit/blob/main/cli/kits/shadcn/skills/shadcn/SKILL.mdView on GitHub Overview
shadcn/ui provides a library of UI components that you copy into your project instead of installing as dependencies. Built on Tailwind CSS and Radix UI primitives, it emphasizes accessibility, type safety, and composability. This guide covers initialization, component addition, and direct in-code customization.
How This Skill Works
Components are copied into your codebase, not installed as dependencies, and are customized directly in code using Tailwind classes and Radix primitives. You gain accessible, type-safe UI elements that you can compose into complex layouts. The pattern includes an init step, add commands for components, and practical usage examples.
When to Use It
- You are building a Tailwind + Radix UI project and need accessible, customizable components
- You prefer copying components into your codebase rather than installing a package
- You need ready-made UI blocks like Button, Card, and Dialog
- You require strong TypeScript typing and ARIA-friendly primitives
- You want to prototype UIs quickly by composing simple primitives into complex widgets
Quick Start
- Step 1: Run npx shadcn@latest init
- Step 2: Add components with npx shadcn@latest add button (and more as needed)
- Step 3: Import components from '@/components/ui/...' and use them in your app
Best Practices
- Copy components into your project rather than installing as dependencies
- Customize components directly in your codebase to preserve flexibility
- Leverage Radix primitives for ARIA support and accessibility
- Maintain full TypeScript support for safety and autocompletion
- Compose simple primitives (Button, Card, Dialog, Form) to build complex UIs
Example Use Cases
- Button with Default, Destructive, and Outline variants as shown in examples
- Card with header, title, description, and content blocks
- Dialog usage with a Trigger button and DialogContent including header and title
- Basic form wired with react-hook-form and zod validation
- Manual component installation by copying code from the docs