- **Component Creation Guide:** Document best practices for creating reusable, accessible components using CVA patterns. Includes guidance on when to compose vs create, decision trees, templates, prop design, testing checklists, and real-world examples. - **Design System README:** Introduce an organized structure for the design system documentation with quick navigation, learning paths, and reference links to key topics. Includes paths for quick starts, layouts, components, forms, and AI setup.
28 KiB
Components
Master the shadcn/ui component library: Learn all variants, composition patterns, and when to use each component. This is your complete reference for building consistent interfaces.
Table of Contents
- Overview
- Core Components
- Form Components
- Feedback Components
- Overlay Components
- Data Display Components
- Composition Patterns
- Quick Reference
Overview
About shadcn/ui
We use shadcn/ui, a collection of accessible, customizable components built on Radix UI primitives.
Key features:
- ✅ Accessible - WCAG AA compliant, keyboard navigation, screen reader support
- ✅ Customizable - Components are copied into your project (not npm dependencies)
- ✅ Composable - Build complex UIs from simple primitives
- ✅ Dark mode - All components work in light and dark modes
- ✅ Type-safe - Full TypeScript support
Installation Method
# Add new components
npx shadcn@latest add button card input dialog
# List available components
npx shadcn@latest add
Installed components (in /src/components/ui/):
- alert, avatar, badge, button, card, checkbox, dialog
- dropdown-menu, input, label, popover, select, separator
- sheet, skeleton, table, tabs, textarea, toast
Core Components
Button
Purpose: Trigger actions, navigate, submit forms
import { Button } from '@/components/ui/button';
// Variants
<Button variant="default">Primary Action</Button>
<Button variant="secondary">Secondary Action</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Subtle Action</Button>
<Button variant="link">Link Style</Button>
<Button variant="destructive">Delete</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon className="h-4 w-4" /></Button>
// States
<Button disabled>Disabled</Button>
<Button loading>Loading...</Button>
// As Link (Next.js)
<Button asChild>
<Link href="/users">View Users</Link>
</Button>
When to use each variant:
| Variant | Use Case | Example |
|---|---|---|
default |
Primary actions, CTAs | Save, Submit, Create |
secondary |
Secondary actions | Cancel, Back |
outline |
Alternative actions | View Details, Edit |
ghost |
Subtle actions in lists | Icon buttons in table rows |
link |
In-text actions | Read more, Learn more |
destructive |
Delete, remove actions | Delete Account, Remove |
Accessibility:
- Always add
aria-labelfor icon-only buttons - Use
disabledfor unavailable actions (not hidden) - Loading state prevents double-submission
Badge
Purpose: Labels, tags, status indicators
import { Badge } from '@/components/ui/badge';
// Variants
<Badge variant="default">New</Badge>
<Badge variant="secondary">Draft</Badge>
<Badge variant="outline">Pending</Badge>
<Badge variant="destructive">Critical</Badge>
// Custom colors (use sparingly)
<Badge className="bg-green-600 text-white">Active</Badge>
<Badge className="bg-yellow-600 text-white">Warning</Badge>
Common patterns:
// Status badge
{user.is_active ? (
<Badge variant="default">Active</Badge>
) : (
<Badge variant="secondary">Inactive</Badge>
)}
// Count badge
<Badge variant="secondary">{itemCount}</Badge>
// Role badge
<Badge variant="outline">{user.role}</Badge>
Avatar
Purpose: User profile pictures, placeholders
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
// With image
<Avatar>
<AvatarImage src="/avatars/user.jpg" alt="John Doe" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>
// Fallback only (initials)
<Avatar>
<AvatarFallback>AB</AvatarFallback>
</Avatar>
// Sizes (custom classes)
<Avatar className="h-8 w-8">...</Avatar>
<Avatar className="h-12 w-12">...</Avatar>
<Avatar className="h-16 w-16">...</Avatar>
Pattern: User menu:
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Card
Purpose: Container for related content, groups information
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter
} from '@/components/ui/card';
// Basic card
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage system users</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter>
<Button>View All</Button>
</CardFooter>
</Card>
// Minimal card (content only)
<Card className="p-6">
<h3 className="text-lg font-semibold mb-2">Quick Stats</h3>
<p className="text-3xl font-bold">1,234</p>
</Card>
Common patterns:
// Card with action button in header
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>Last 7 days</CardDescription>
</div>
<Button variant="outline" size="sm">View All</Button>
</div>
</CardHeader>
<CardContent>{/* content */}</CardContent>
</Card>
// Dashboard metric card
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total Revenue
</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>
Separator
Purpose: Visual divider between content sections
import { Separator } from '@/components/ui/separator';
// Horizontal (default)
<div className="space-y-4">
<div>Section 1</div>
<Separator />
<div>Section 2</div>
</div>
// Vertical
<div className="flex gap-4">
<div>Left</div>
<Separator orientation="vertical" className="h-12" />
<div>Right</div>
</div>
// Decorative (for screen readers)
<Separator decorative />
Form Components
Input
Purpose: Text input, email, password, etc.
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
// Basic input
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
/>
</div>
// With error state
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error' : undefined}
className={errors.password ? 'border-destructive' : ''}
/>
{errors.password && (
<p id="password-error" className="text-sm text-destructive">
{errors.password.message}
</p>
)}
</div>
// Disabled
<Input disabled placeholder="Disabled input" />
// Read-only
<Input readOnly value="Read-only value" />
Input types:
text- Default text inputemail- Email addresspassword- Password fieldnumber- Numeric inputtel- Telephone numberurl- URL inputsearch- Search field
See Forms Guide for complete form patterns
Textarea
Purpose: Multi-line text input
import { Textarea } from '@/components/ui/textarea';
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Enter description..."
rows={4}
/>
</div>
// With character count
<div className="space-y-2">
<Label htmlFor="bio">Bio</Label>
<Textarea
id="bio"
maxLength={500}
value={bio}
onChange={(e) => setBio(e.target.value)}
/>
<p className="text-xs text-muted-foreground text-right">
{bio.length} / 500 characters
</p>
</div>
Select
Purpose: Dropdown selection
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectGroup,
SelectLabel
} from '@/components/ui/select';
<div className="space-y-2">
<Label htmlFor="role">Role</Label>
<Select onValueChange={setRole} defaultValue={role}>
<SelectTrigger id="role">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="guest">Guest</SelectItem>
</SelectContent>
</Select>
</div>
// With groups
<Select>
<SelectTrigger>
<SelectValue placeholder="Select timezone" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>North America</SelectLabel>
<SelectItem value="est">Eastern Time</SelectItem>
<SelectItem value="cst">Central Time</SelectItem>
<SelectItem value="pst">Pacific Time</SelectItem>
</SelectGroup>
<SelectGroup>
<SelectLabel>Europe</SelectLabel>
<SelectItem value="gmt">GMT</SelectItem>
<SelectItem value="cet">CET</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
Checkbox
Purpose: Boolean selection, multi-select
import { Checkbox } from '@/components/ui/checkbox';
// Basic checkbox
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms and conditions</Label>
</div>
// With controlled state
<div className="flex items-center space-x-2">
<Checkbox
id="newsletter"
checked={subscribed}
onCheckedChange={setSubscribed}
/>
<Label htmlFor="newsletter">Subscribe to newsletter</Label>
</div>
// Indeterminate state (select all)
<Checkbox
checked={selectedAll}
onCheckedChange={handleSelectAll}
indeterminate={someSelected}
/>
Label
Purpose: Form field labels (accessibility)
import { Label } from '@/components/ui/label';
// Basic label
<Label htmlFor="email">Email Address</Label>
<Input id="email" type="email" />
// With required indicator
<Label htmlFor="password">
Password <span className="text-destructive">*</span>
</Label>
<Input id="password" type="password" required />
// With helper text
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Input id="username" />
<p className="text-sm text-muted-foreground">
Choose a unique username (3-20 characters)
</p>
</div>
Accessibility: Always associate labels with inputs using htmlFor and id.
Feedback Components
Alert
Purpose: Important messages, notifications
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle, CheckCircle, Info } from 'lucide-react';
// Info alert (default)
<Alert>
<Info className="h-4 w-4" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
This is an informational message.
</AlertDescription>
</Alert>
// Error alert
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
// Success alert (custom)
<Alert className="bg-green-50 text-green-900 border-green-200">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Success!</AlertTitle>
<AlertDescription>
Your changes have been saved.
</AlertDescription>
</Alert>
// Minimal alert (description only)
<Alert>
<AlertDescription>
Session will expire in 5 minutes.
</AlertDescription>
</Alert>
When to use:
- ✅ Form-level errors
- ✅ Important warnings
- ✅ Success confirmations (inline)
- ❌ Don't use for transient notifications (use Toast)
Toast (Sonner)
Purpose: Transient notifications, feedback
import { toast } from 'sonner';
// Success
toast.success('User created successfully');
// Error
toast.error('Failed to delete user');
// Info
toast.info('Processing your request...');
// Warning
toast.warning('This action cannot be undone');
// Loading (with promise)
toast.promise(
saveChanges(),
{
loading: 'Saving changes...',
success: 'Changes saved!',
error: 'Failed to save changes',
}
);
// Custom with action
toast('Event has been created', {
description: 'Monday, January 3rd at 6:00pm',
action: {
label: 'Undo',
onClick: () => undoAction(),
},
});
// Dismiss all toasts
toast.dismiss();
When to use:
- ✅ Action confirmations (saved, deleted)
- ✅ Background task updates
- ✅ Temporary errors
- ❌ Critical errors (use Alert)
- ❌ Form validation errors (use inline errors)
Skeleton
Purpose: Loading placeholders
import { Skeleton } from '@/components/ui/skeleton';
// Basic skeleton
<Skeleton className="h-12 w-full" />
// Card skeleton
<Card>
<CardHeader>
<Skeleton className="h-6 w-1/3" />
<Skeleton className="h-4 w-1/2 mt-2" />
</CardHeader>
<CardContent className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</CardContent>
</Card>
// Avatar skeleton
<Skeleton className="h-12 w-12 rounded-full" />
// Table skeleton
<Table>
<TableBody>
{[...Array(5)].map((_, i) => (
<TableRow key={i}>
<TableCell><Skeleton className="h-4 w-full" /></TableCell>
<TableCell><Skeleton className="h-4 w-full" /></TableCell>
<TableCell><Skeleton className="h-4 w-20" /></TableCell>
</TableRow>
))}
</TableBody>
</Table>
Pattern: Loading states:
{isLoading ? (
<Skeleton className="h-48 w-full" />
) : (
<div>{content}</div>
)}
Overlay Components
Dialog
Purpose: Modal windows, confirmations, forms
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogTrigger,
DialogClose
} from '@/components/ui/dialog';
// Basic dialog
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>
Are you sure you want to proceed? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button variant="destructive" onClick={handleConfirm}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
// Controlled dialog
const [isOpen, setIsOpen] = useState(false);
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create User</DialogTitle>
</DialogHeader>
<UserForm
onSuccess={() => {
setIsOpen(false);
toast.success('User created');
}}
/>
</DialogContent>
</Dialog>
Accessibility:
- Escape key closes dialog
- Focus trapped inside dialog
- Returns focus to trigger on close
Dropdown Menu
Purpose: Action menus, user menus, context menus
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuGroup,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem
} from '@/components/ui/dropdown-menu';
// Basic dropdown
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleEdit}>
<Edit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={handleDuplicate}>
<Copy className="mr-2 h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
// User menu
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{user.initials}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
// With checkboxes
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">View Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>Show Columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={showName}
onCheckedChange={setShowName}
>
Name
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showEmail}
onCheckedChange={setShowEmail}
>
Email
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
Popover
Purpose: Contextual information, mini-forms
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@/components/ui/popover';
// Basic popover
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent>
<div className="space-y-2">
<h4 className="font-medium">Popover Title</h4>
<p className="text-sm text-muted-foreground">
Popover content goes here
</p>
</div>
</PopoverContent>
</Popover>
// With form
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Add Tag</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="tag">Tag Name</Label>
<Input id="tag" placeholder="Enter tag name" />
</div>
<Button className="w-full">Add Tag</Button>
</div>
</PopoverContent>
</Popover>
Sheet
Purpose: Side panels, mobile navigation
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
SheetTrigger,
SheetFooter,
SheetClose
} from '@/components/ui/sheet';
// Basic sheet
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open Sheet</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Sheet Title</SheetTitle>
<SheetDescription>
Sheet description goes here
</SheetDescription>
</SheetHeader>
<div className="py-4">
{/* Content */}
</div>
<SheetFooter>
<SheetClose asChild>
<Button>Close</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
// Side variants
<SheetContent side="left">...</SheetContent>
<SheetContent side="right">...</SheetContent> {/* Default */}
<SheetContent side="top">...</SheetContent>
<SheetContent side="bottom">...</SheetContent>
// Mobile navigation pattern
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu className="h-6 w-6" />
</Button>
</SheetTrigger>
<SheetContent side="left">
<nav className="flex flex-col gap-4">
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
</SheetContent>
</Sheet>
Data Display Components
Table
Purpose: Display tabular data
import {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption
} from '@/components/ui/table';
<Table>
<TableCaption>A list of your recent invoices</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">{invoice.id}</TableCell>
<TableCell>{invoice.status}</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,500.00</TableCell>
</TableRow>
</TableFooter>
</Table>
For advanced tables (sorting, filtering, pagination), use TanStack Table with react-hook-form.
Tabs
Purpose: Organize content into switchable panels
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
<Tabs defaultValue="profile">
<TabsList>
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="sessions">Sessions</TabsTrigger>
</TabsList>
<TabsContent value="profile">
<ProfileSettings />
</TabsContent>
<TabsContent value="password">
<PasswordSettings />
</TabsContent>
<TabsContent value="sessions">
<SessionManagement />
</TabsContent>
</Tabs>
// Full-width tabs
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
<TabsTrigger value="tab3">Tab 3</TabsTrigger>
</TabsList>
Composition Patterns
Pattern 1: Card + Table
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Users</CardTitle>
<CardDescription>Manage system users</CardDescription>
</div>
<Button onClick={() => router.push('/users/new')}>
<Plus className="mr-2 h-4 w-4" />
Create User
</Button>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
Pattern 2: Dialog + Form
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>Create User</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New User</DialogTitle>
<DialogDescription>
Add a new user to the system
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" {...register('name')} />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" {...register('email')} />
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>
Cancel
</Button>
<Button type="submit">Create</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
Pattern 3: Tabs + Cards
<Card>
<CardHeader>
<CardTitle>Account Settings</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="profile">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="profile">Profile</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
<TabsTrigger value="sessions">Sessions</TabsTrigger>
</TabsList>
<TabsContent value="profile" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" />
</div>
<Button>Save Changes</Button>
</TabsContent>
<TabsContent value="password" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="current">Current Password</Label>
<Input id="current" type="password" />
</div>
<Button>Update Password</Button>
</TabsContent>
<TabsContent value="sessions">
<SessionList />
</TabsContent>
</Tabs>
</CardContent>
</Card>
Pattern 4: Dropdown + Table Row Actions
<Table>
<TableBody>
{users.map(user => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEdit(user)}>
<Edit className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleView(user)}>
<Eye className="mr-2 h-4 w-4" />
View Details
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => handleDelete(user)}
className="text-destructive"
>
<Trash className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
Quick Reference
Component Decision Tree
Need to trigger an action? → Button
Need to show status/label? → Badge
Need to display user image? → Avatar
Need to group content? → Card
Need to divide sections? → Separator
Need text input? → Input
Need multi-line input? → Textarea
Need dropdown selection? → Select
Need boolean option? → Checkbox
Need to label a field? → Label
Need important message? → Alert
Need transient notification? → Toast
Need loading placeholder? → Skeleton
Need modal/confirmation? → Dialog
Need action menu? → Dropdown Menu
Need contextual info? → Popover
Need side panel? → Sheet
Need tabular data? → Table
Need switchable panels? → Tabs
Component Variants Quick Reference
Button:
default- Primary actionsecondary- Secondary actionoutline- Alternative actionghost- Subtle actionlink- In-text actiondestructive- Delete/remove
Badge:
default- Blue (new, active)secondary- Gray (draft, inactive)outline- Bordered (pending)destructive- Red (critical, error)
Alert:
default- Infodestructive- Error
Next Steps
- Interactive Examples: Component Showcase
- Form Patterns: Forms Guide
- Component Creation: Build custom components
- Accessibility: Accessibility Guide
Related Documentation:
- Quick Start - Essential patterns
- Foundations - Colors, typography, spacing
- Layouts - Layout patterns
- Forms - Form validation and patterns
External Resources:
Last Updated: November 2, 2025