UI logoUIUI Components

Composition

Patterns for grouping primitives, overlays, cards, feedback, and loading UI when building with shadcn/ui in this project.

Following these composition rules keeps layouts predictable, improves accessibility, and avoids one-off markup that drifts from the design system. They apply to new features and when extending existing components.

Want a portable copy for your repo or internal docs? Download this guide as Markdown.

Grouping menu-like UIs

Never render items directly inside the content container without a group wrapper.

Incorrect:

<SelectContent>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
</SelectContent>

Correct:

<SelectContent>
    <SelectGroup>
        <SelectItem value="apple">Apple</SelectItem>
        <SelectItem value="banana">Banana</SelectItem>
    </SelectGroup>
</SelectContent>

This applies to all group-based components:

ItemGroup
SelectItem, SelectLabelSelectGroup
DropdownMenuItem, DropdownMenuLabel, DropdownMenuSubDropdownMenuGroup
MenubarItemMenubarGroup
ContextMenuItemContextMenuGroup
CommandItemCommandGroup

Card structure

Use full composition — do not put everything in CardContent alone:

<Card>
    <CardHeader>
        <CardTitle>Team Members</CardTitle>
        <CardDescription>Manage your team.</CardDescription>
    </CardHeader>
    <CardContent>...</CardContent>
    <CardFooter>
        <Button>Invite</Button>
    </CardFooter>
</Card>

Tabs

TabsTrigger must live inside TabsList. Never place triggers directly under Tabs:

<Tabs defaultValue="account">
    <TabsList>
        <TabsTrigger value="account">Account</TabsTrigger>
        <TabsTrigger value="password">Password</TabsTrigger>
    </TabsList>
    <TabsContent value="account">...</TabsContent>
</Tabs>

Choosing overlay components

Use caseComponent
Focused task that requires inputDialog
Destructive action confirmationAlertDialog
Side panel with details or filtersSheet
Mobile-first bottom panelDrawer
Quick info on hoverHoverCard
Small contextual content on clickPopover

Dialog, Sheet, and Drawer titles

DialogTitle, SheetTitle, and DrawerTitle are required for accessibility. Use className="sr-only" when the title should not be visible.

<DialogContent>
    <DialogHeader>
        <DialogTitle>Edit Profile</DialogTitle>
        <DialogDescription>Update your profile.</DialogDescription>
    </DialogHeader>
    ...
</DialogContent>

Callouts

Use Alert for inline callouts:

<Alert>
    <AlertTitle>Warning</AlertTitle>
    <AlertDescription>Something needs attention.</AlertDescription>
</Alert>

Empty states

Use the Empty component and its subcomponents:

<Empty>
    <EmptyHeader>
        <EmptyMedia variant="icon">
            <FolderIcon />
        </EmptyMedia>
        <EmptyTitle>No projects yet</EmptyTitle>
        <EmptyDescription>
            Get started by creating a new project.
        </EmptyDescription>
    </EmptyHeader>
    <EmptyContent>
        <Button>Create Project</Button>
    </EmptyContent>
</Empty>

Toast notifications

Use sonner for toasts:

import { toast } from "sonner"

toast.success("Changes saved.")
toast.error("Something went wrong.")
toast("File deleted.", {
    action: { label: "Undo", onClick: () => undoDelete() },
})

Buttons and loading state

Button has no isPending or isLoading prop. Compose with Spinner, data-icon, and disabled:

<Button disabled>
    <Spinner data-icon="inline-start" />
    Saving...
</Button>

Avatars

Always include AvatarFallback for when the image fails to load:

<Avatar>
    <AvatarImage src="/avatar.png" alt="User" />
    <AvatarFallback>JD</AvatarFallback>
</Avatar>

Prefer primitives over custom markup

Instead ofUse
<hr> or <div className="border-t"><Separator />
<div className="animate-pulse"> with styled divs<Skeleton className="h-4 w-3/4" />
<span className="rounded-full bg-green-100 ..."><Badge variant="secondary">

See also

Browse ready-made component examples for specimens that follow these patterns end to end.

On this page