Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Add, manage, and compose shadcn/ui components with correct patterns, styling, and CLI workflows.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/composition.md
1# Component Composition23## Contents45- Items always inside their Group component6- Callouts use Alert7- Empty states use Empty component8- Toast notifications use sonner9- Choosing between overlay components10- Dialog, Sheet, and Drawer always need a Title11- Card structure12- Button has no isPending or isLoading prop13- TabsTrigger must be inside TabsList14- Avatar always needs AvatarFallback15- Use Separator instead of raw hr or border divs16- Use Skeleton for loading placeholders17- Use Badge instead of custom styled spans1819---2021## Items always inside their Group component2223Never render items directly inside the content container.2425**Incorrect:**2627```tsx28<SelectContent>29<SelectItem value="apple">Apple</SelectItem>30<SelectItem value="banana">Banana</SelectItem>31</SelectContent>32```3334**Correct:**3536```tsx37<SelectContent>38<SelectGroup>39<SelectItem value="apple">Apple</SelectItem>40<SelectItem value="banana">Banana</SelectItem>41</SelectGroup>42</SelectContent>43```4445This applies to all group-based components:4647| Item | Group |48|------|-------|49| `SelectItem`, `SelectLabel` | `SelectGroup` |50| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |51| `MenubarItem` | `MenubarGroup` |52| `ContextMenuItem` | `ContextMenuGroup` |53| `CommandItem` | `CommandGroup` |5455---5657## Callouts use Alert5859```tsx60<Alert>61<AlertTitle>Warning</AlertTitle>62<AlertDescription>Something needs attention.</AlertDescription>63</Alert>64```6566---6768## Empty states use Empty component6970```tsx71<Empty>72<EmptyHeader>73<EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>74<EmptyTitle>No projects yet</EmptyTitle>75<EmptyDescription>Get started by creating a new project.</EmptyDescription>76</EmptyHeader>77<EmptyContent>78<Button>Create Project</Button>79</EmptyContent>80</Empty>81```8283---8485## Toast notifications use sonner8687```tsx88import { toast } from "sonner"8990toast.success("Changes saved.")91toast.error("Something went wrong.")92toast("File deleted.", {93action: { label: "Undo", onClick: () => undoDelete() },94})95```9697---9899## Choosing between overlay components100101| Use case | Component |102|----------|-----------|103| Focused task that requires input | `Dialog` |104| Destructive action confirmation | `AlertDialog` |105| Side panel with details or filters | `Sheet` |106| Mobile-first bottom panel | `Drawer` |107| Quick info on hover | `HoverCard` |108| Small contextual content on click | `Popover` |109110---111112## Dialog, Sheet, and Drawer always need a Title113114`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className="sr-only"` if visually hidden.115116```tsx117<DialogContent>118<DialogHeader>119<DialogTitle>Edit Profile</DialogTitle>120<DialogDescription>Update your profile.</DialogDescription>121</DialogHeader>122...123</DialogContent>124```125126---127128## Card structure129130Use full composition — don't dump everything into `CardContent`:131132```tsx133<Card>134<CardHeader>135<CardTitle>Team Members</CardTitle>136<CardDescription>Manage your team.</CardDescription>137</CardHeader>138<CardContent>...</CardContent>139<CardFooter>140<Button>Invite</Button>141</CardFooter>142</Card>143```144145---146147## Button has no isPending or isLoading prop148149Compose with `Spinner` + `data-icon` + `disabled`:150151```tsx152<Button disabled>153<Spinner data-icon="inline-start" />154Saving...155</Button>156```157158---159160## TabsTrigger must be inside TabsList161162Never render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:163164```tsx165<Tabs defaultValue="account">166<TabsList>167<TabsTrigger value="account">Account</TabsTrigger>168<TabsTrigger value="password">Password</TabsTrigger>169</TabsList>170<TabsContent value="account">...</TabsContent>171</Tabs>172```173174---175176## Avatar always needs AvatarFallback177178Always include `AvatarFallback` for when the image fails to load:179180```tsx181<Avatar>182<AvatarImage src="/avatar.png" alt="User" />183<AvatarFallback>JD</AvatarFallback>184</Avatar>185```186187---188189## Use existing components instead of custom markup190191| Instead of | Use |192|---|---|193| `<hr>` or `<div className="border-t">` | `<Separator />` |194| `<div className="animate-pulse">` with styled divs | `<Skeleton className="h-4 w-3/4" />` |195| `<span className="rounded-full bg-green-100 ...">` | `<Badge variant="secondary">` |196