# Component Composition ## Contents - Items always inside their Group component - Callouts use Alert - Empty states use Empty component - Toast notifications use sonner - Choosing between overlay components - Dialog, Sheet, and Drawer always need a Title - Card structure - Button has no isPending or isLoading prop - TabsTrigger must be inside TabsList - Avatar always needs AvatarFallback - Use Separator instead of raw hr or border divs - Use Skeleton for loading placeholders - Use Badge instead of custom styled spans --- ## Items always inside their Group component Never render items directly inside the content container. **Incorrect:** ```tsx Apple Banana ``` **Correct:** ```tsx Apple Banana ``` This applies to all group-based components: | Item | Group | |------|-------| | `SelectItem`, `SelectLabel` | `SelectGroup` | | `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` | | `MenubarItem` | `MenubarGroup` | | `ContextMenuItem` | `ContextMenuGroup` | | `CommandItem` | `CommandGroup` | --- ## Callouts use Alert ```tsx Warning Something needs attention. ``` --- ## Empty states use Empty component ```tsx No projects yet Get started by creating a new project. ``` --- ## Toast notifications use sonner ```tsx import { toast } from "sonner" toast.success("Changes saved.") toast.error("Something went wrong.") toast("File deleted.", { action: { label: "Undo", onClick: () => undoDelete() }, }) ``` --- ## Choosing between overlay components | Use case | Component | |----------|-----------| | Focused task that requires input | `Dialog` | | Destructive action confirmation | `AlertDialog` | | Side panel with details or filters | `Sheet` | | Mobile-first bottom panel | `Drawer` | | Quick info on hover | `HoverCard` | | Small contextual content on click | `Popover` | --- ## Dialog, Sheet, and Drawer always need a Title `DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden. ```tsx Edit Profile Update your profile. ... ``` --- ## Card structure Use full composition — don't dump everything into `CardContent`: ```tsx Team Members Manage your team. ... ``` --- ## Button has no isPending or isLoading prop Compose with `Spinner` + `data-icon` + `disabled`: ```tsx ``` --- ## TabsTrigger must be inside TabsList Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`: ```tsx Account Password ... ``` --- ## Avatar always needs AvatarFallback Always include `AvatarFallback` for when the image fails to load: ```tsx JD ``` --- ## Use existing components instead of custom markup | Instead of | Use | |---|---| | `
` or `
` | `` | | `
` with styled divs | `` | | `` | `` |