Building with shadcn/ui
Quick Start
Initialize and add components
npx shadcn-ui@latest init npx shadcn-ui@latest add button card form input dialog
import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export function Example() { return ( <Card> <CardHeader> <CardTitle>Welcome</CardTitle> </CardHeader> <CardContent className="flex gap-4"> <Button>Primary</Button> <Button variant="outline">Outline</Button> </CardContent> </Card> ); }
Features
Feature Description Guide
Button Variants default, secondary, destructive, outline, ghost, link ref/button.md
Form Integration React Hook Form + Zod validation pattern ref/forms.md
Dialog/Sheet Modal dialogs and slide-out panels ref/dialogs.md
Data Display Table, Tabs, Accordion components ref/data-display.md
Navigation DropdownMenu, Command palette, NavigationMenu ref/navigation.md
Feedback Toast notifications with useToast hook ref/toast.md
Common Patterns
Form with Validation
const formSchema = z.object({ email: z.string().email("Invalid email"), name: z.string().min(2, "Name must be at least 2 characters"), });
export function ProfileForm() { const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { email: "", name: "" }, });
return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl><Input {...field} /></FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl><Input {...field} /></FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" disabled={form.formState.isSubmitting}> {form.formState.isSubmitting ? "Saving..." : "Save"} </Button> </form> </Form> ); }
Dialog with Form
export function EditDialog({ onSave }: { onSave: (data: Data) => void }) { return ( <Dialog> <DialogTrigger asChild> <Button variant="outline">Edit Profile</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Edit Profile</DialogTitle> <DialogDescription>Update your profile information.</DialogDescription> </DialogHeader> <div className="grid gap-4 py-4"> <div className="grid grid-cols-4 items-center gap-4"> <Label htmlFor="name" className="text-right">Name</Label> <Input id="name" className="col-span-3" /> </div> </div> <DialogFooter> <DialogClose asChild><Button variant="outline">Cancel</Button></DialogClose> <Button onClick={() => onSave(data)}>Save</Button> </DialogFooter> </DialogContent> </Dialog> ); }
Toast Notifications
import { useToast } from "@/components/ui/use-toast";
export function SaveButton() { const { toast } = useToast();
const handleSave = async () => { try { await saveData(); toast({ title: "Success", description: "Changes saved." }); } catch { toast({ variant: "destructive", title: "Error", description: "Failed to save." }); } };
return <Button onClick={handleSave}>Save</Button>; }
Best Practices
Do Avoid
Install only components you need Modifying generated component files directly
Use cn() utility for class merging Skipping form validation
Extend components with composition Overriding styles without good reason
Follow React Hook Form patterns Using inline styles
Use TypeScript for type safety Skipping loading and error states
Test component accessibility Ignoring keyboard navigation