- Consolidated multi-line arguments into single lines where appropriate in `useAuth`. - Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`). - Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`). - Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
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