Builds accessible UI with shadcn/ui including component installation, customization, theming, and Radix primitives. Use when adding UI components, customizing design systems, implementing accessible interfaces, or building component libraries.
Installs and configures shadcn/ui components for accessible, customizable React UIs. Use when building forms, dialogs, tables, or any UI component that needs consistent styling and accessibility.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Beautifully designed, accessible, and customizable React components built on Radix UI primitives.
Initialize in project:
npx shadcn@latest init
Configuration prompts:
Would you like to use TypeScript? yes
Which style would you like to use? Default
Which color would you like to use as base color? Slate
Where is your global CSS file? app/globals.css
Would you like to use CSS variables? yes
Where is your tailwind.config.js? tailwind.config.ts
Configure the import alias for components? @/components
Configure the import alias for utils? @/lib/utils
Add components:
npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add input
npx shadcn@latest add form
src/
components/
ui/ # shadcn/ui components
button.tsx
card.tsx
input.tsx
lib/
utils.ts # cn() utility
app/
globals.css # CSS variables
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
npx shadcn@latest add button
import { Button } from '@/components/ui/button';
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><IconPlus /></Button>
// States
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/about">As Link</Link>
</Button>
// With loading state
<Button disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Submit
</Button>
npx shadcn@latest add card
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here.</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
npx shadcn@latest add form input label
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';
const formSchema = z.object({
username: z.string().min(2).max(50),
email: z.string().email(),
});
type FormValues = z.infer<typeof formSchema>;
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-6">
<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>
);
}
npx shadcn@latest add dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from '@/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">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">
<Input placeholder="Name" />
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
function ControlledDialog() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button
onClick={() => {
handleAction();
setOpen(false);
}}
>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
npx shadcn@latest add select
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
<Select>
<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">Orange</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
// Controlled
const [value, setValue] = useState('');
<Select value={value} onValueChange={setValue}>
{/* ... */}
</Select>
npx shadcn@latest add dropdown-menu
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleEdit()}>
Edit
<DropdownMenuShortcut>Cmd+E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDuplicate()}>
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-red-600"
onClick={() => handleDelete()}
>
Delete
<DropdownMenuShortcut>Cmd+Delete</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
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>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</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>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
npx shadcn@latest add toast
// In layout
import { Toaster } from '@/components/ui/toaster';
export default function Layout({ children }) {
return (
<>
{children}
<Toaster />
</>
);
}
// Usage
import { useToast } from '@/components/ui/use-toast';
function MyComponent() {
const { toast } = useToast();
return (
<Button
onClick={() => {
toast({
title: 'Success!',
description: 'Your action was completed.',
});
}}
>
Show Toast
</Button>
);
}
// Variants
toast({
variant: 'destructive',
title: 'Error',
description: 'Something went wrong.',
});
toast({
title: 'Scheduled',
description: 'Event scheduled for Friday.',
action: (
<ToastAction altText="Undo">Undo</ToastAction>
),
});
npx shadcn@latest add alert-dialog
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>
Yes, delete account
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
/* 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%;
/* ... other dark mode values */
}
}
// tailwind.config.ts
export default {
theme: {
extend: {
colors: {
brand: {
DEFAULT: 'hsl(var(--brand))',
foreground: 'hsl(var(--brand-foreground))',
},
},
},
},
};
/* globals.css */
:root {
--brand: 262.1 83.3% 57.8%;
--brand-foreground: 210 40% 98%;
}
// components/ui/button.tsx
const buttonVariants = cva(
'inline-flex items-center justify-center...',
{
variants: {
variant: {
default: '...',
destructive: '...',
// Add custom variant
success:
'bg-green-500 text-white hover:bg-green-600',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
// Add custom size
xl: 'h-14 rounded-lg px-10 text-lg',
},
},
}
);
// Usage
<Button variant="success" size="xl">Big Success</Button>
// components/stat-card.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
interface StatCardProps {
title: string;
value: string | number;
description?: string;
trend?: 'up' | 'down';
className?: string;
}
export function StatCard({
title,
value,
description,
trend,
className,
}: StatCardProps) {
return (
<Card className={cn('', className)}>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{description && (
<p
className={cn(
'text-xs',
trend === 'up' && 'text-green-600',
trend === 'down' && 'text-red-600',
!trend && 'text-muted-foreground'
)}
>
{description}
</p>
)}
</CardContent>
</Card>
);
}
// 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>
);
}
| Mistake | Fix |
|---|---|
| Importing from npm | Use local components |
| Removing accessibility | Keep Radix primitives |
| Over-customizing | Start with defaults |
| Ignoring dark mode | Test both themes |
| Hardcoded colors | Use CSS variables |
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.