| name | shadcn |
| description | Implement shadcn/ui components for PhotoVault with accessibility and theming. Use when working with UI components, forms, dialogs, buttons, cards, tables, Tailwind styling, or fixing accessibility issues. Includes PhotoVault theme system and established UI patterns. |
⚠️ MANDATORY WORKFLOW - DO NOT SKIP
When this skill activates, you MUST follow the expert workflow before writing any code:
Spawn Domain Expert using the Task tool with this prompt:
Read the expert prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\shadcn-expert.md Then research the codebase and write an implementation plan to: docs/claude/plans/ui-[task-name]-plan.md Task: [describe the user's request]Spawn QA Critic after expert returns, using Task tool:
Read the QA critic prompt at: C:\Users\natha\Stone-Fence-Brain\VENTURES\PhotoVault\claude\experts\qa-critic-expert.md Review the plan at: docs/claude/plans/ui-[task-name]-plan.md Write critique to: docs/claude/plans/ui-[task-name]-critique.mdPresent BOTH plan and critique to user - wait for approval before implementing
DO NOT read files and start coding. DO NOT rationalize that "this is simple." Follow the workflow.
shadcn/ui Integration
Core Principles
Accessibility First, Always
Every interactive component MUST be keyboard navigable and screen reader friendly. shadcn/ui is built on Radix primitives which handle most accessibility out of the box.
<Button aria-label="Close dialog" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
Composition Over Configuration
shadcn/ui components are meant to be composed, not configured with dozens of props.
// ✅ RIGHT: Compose components
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content here</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
// ❌ WRONG: Mega-component with too many props
<Card title="Title" description="..." content="..." footerButton="Action" />
Always Use Semantic Color Tokens
Never hardcode colors. Use semantic tokens for theming support.
// ❌ WRONG: Hardcoded colors break theming
<div className="bg-white text-black" />
// ✅ RIGHT: Semantic tokens
<div className="bg-background text-foreground" />
Anti-Patterns
Not forwarding refs on custom components
// WRONG: Refs don't work
const CustomButton = ({ className, ...props }) => {
return <Button className={className} {...props} />
}
// RIGHT: Forward refs
const CustomButton = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof Button>
>(({ className, ...props }, ref) => {
return <Button ref={ref} className={className} {...props} />
})
CustomButton.displayName = "CustomButton"
Not using cn() for class merging
// WRONG: Classes override unpredictably
<Button className={`${baseClasses} ${conditionalClasses}`}>
// RIGHT: Use cn() for proper merging
import { cn } from "@/lib/utils"
<Button className={cn(baseClasses, conditionalClasses)}>
Nesting interactive elements
// WRONG: Button inside clickable card (accessibility violation)
<Card onClick={handleClick}>
<Button onClick={handleAction}>Action</Button>
</Card>
// RIGHT: Separate interactive areas
<Card>
<CardContent onClick={handleClick}>...</CardContent>
<CardFooter>
<Button onClick={handleAction}>Action</Button>
</CardFooter>
</Card>
Not connecting labels to inputs
// WRONG: Label not connected
<Label>Email</Label>
<Input type="email" />
// RIGHT: Use FormField pattern
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
Button Variants
<Button>Save Changes</Button> // Primary
<Button variant="secondary">Cancel</Button> // Secondary
<Button variant="destructive">Delete</Button> // Destructive
<Button variant="ghost">Edit</Button> // Subtle
<Button variant="outline">View Details</Button> // Outline
// Icon button with accessibility
<Button variant="ghost" size="icon" aria-label="Settings">
<Settings className="h-4 w-4" />
</Button>
// Loading state
<Button disabled>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Please wait
</Button>
Dialog Pattern
import {
Dialog, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from "@/components/ui/dialog"
function ConfirmDialog({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive">Delete Item</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>
)
}
Form Pattern (react-hook-form + zod)
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email("Please enter a valid email"),
name: z.string().min(2, "Name must be at least 2 characters"),
})
function ContactForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: { email: "", name: "" },
})
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>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Submitting..." : "Submit"}
</Button>
</form>
</Form>
)
}
Toast Notifications (Sonner)
import { toast } from "sonner"
toast.success("Changes saved successfully")
toast.error("Failed to save changes")
// Promise toast
toast.promise(saveData(), {
loading: "Saving...",
success: "Data saved!",
error: "Could not save data",
})
PhotoVault Configuration
Theme System
PhotoVault has 5 color themes in src/lib/themes.ts:
- Warm Gallery - Cream background, terracotta primary (default)
- Cool Professional - Cool slate, indigo primary
- Gallery Dark - Warm charcoal, amber accents
- Soft Sage - Sage green, emerald + pink accents
- Original Teal - Original PhotoVault theme
PhotoVault UI Patterns
Gallery Card:
<Card className="overflow-hidden transition-shadow hover:shadow-lg">
<div className="aspect-[4/3] relative">
<Image src={coverImage} alt={galleryName} fill className="object-cover" />
</div>
<CardContent className="p-4">
<h3 className="font-semibold truncate">{galleryName}</h3>
<p className="text-sm text-muted-foreground">{photoCount} photos</p>
</CardContent>
</Card>
Paywall UI:
<div className="text-center py-12 px-4">
<Lock className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h2 className="text-2xl font-bold mb-2">Gallery Access Required</h2>
<p className="text-muted-foreground mb-6">Pay to unlock all photos</p>
<Button size="lg" onClick={handlePayment}>Pay Now - ${price}</Button>
</div>
Semantic Color Tokens
| Token | Purpose |
|---|---|
bg-background |
Page background |
bg-card |
Card backgrounds |
bg-primary |
Primary buttons |
bg-secondary |
Secondary elements |
bg-destructive |
Delete/error actions |
text-muted-foreground |
Subtle text |
border-border |
Borders |
Component Installation
npx shadcn@latest add button card dialog form input
Debugging Checklist
- Are you using semantic color tokens? Check for hardcoded colors
- Is cn() being used for class merging?
- Are refs being forwarded in custom components?
- Is the component accessible? Test with keyboard
- Are form fields properly connected to labels?