- **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.
875 lines
20 KiB
Markdown
875 lines
20 KiB
Markdown
# Component Creation Guide
|
|
|
|
**Learn when to create custom components vs composing existing ones**, and master the patterns for building reusable, accessible components with variants using CVA (class-variance-authority).
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [When to Create vs Compose](#when-to-create-vs-compose)
|
|
2. [Component Templates](#component-templates)
|
|
3. [Variant Patterns (CVA)](#variant-patterns-cva)
|
|
4. [Prop Design](#prop-design)
|
|
5. [Testing Checklist](#testing-checklist)
|
|
6. [Real-World Examples](#real-world-examples)
|
|
|
|
---
|
|
|
|
## When to Create vs Compose
|
|
|
|
### The Golden Rule
|
|
|
|
**80% of the time, you should COMPOSE existing shadcn/ui components.**
|
|
|
|
Only create custom components when:
|
|
1. ✅ You're reusing the same composition 3+ times
|
|
2. ✅ The pattern has complex business logic
|
|
3. ✅ You need variants beyond what shadcn/ui provides
|
|
|
|
---
|
|
|
|
### Decision Tree
|
|
|
|
```
|
|
Do you need a UI element?
|
|
│
|
|
├─ Does shadcn/ui have this component?
|
|
│ │
|
|
│ ├─YES─> Use it directly
|
|
│ │ <Button>Action</Button>
|
|
│ │
|
|
│ └─NO──> Can you compose multiple shadcn/ui components?
|
|
│ │
|
|
│ ├─YES─> Compose them inline first
|
|
│ │ <Card>
|
|
│ │ <CardHeader>...</CardHeader>
|
|
│ │ </Card>
|
|
│ │
|
|
│ └─NO──> Are you using this composition 3+ times?
|
|
│ │
|
|
│ ├─NO──> Keep composing inline
|
|
│ │
|
|
│ └─YES─> Create a custom component
|
|
│ function MyComponent() { ... }
|
|
```
|
|
|
|
---
|
|
|
|
### ✅ GOOD: Compose First
|
|
|
|
```tsx
|
|
// ✅ CORRECT - Compose inline
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{title}</CardTitle>
|
|
<CardDescription>{description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>{content}</p>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button onClick={onAction}>{actionLabel}</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
```
|
|
|
|
**Why this is good:**
|
|
- Simple and direct
|
|
- Easy to customize per use case
|
|
- No abstraction overhead
|
|
- Clear what's happening
|
|
|
|
---
|
|
|
|
### ❌ BAD: Over-Abstracting Too Soon
|
|
|
|
```tsx
|
|
// ❌ WRONG - Premature abstraction
|
|
function ContentCard({ title, description, content, actionLabel, onAction }: Props) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{title}</CardTitle>
|
|
<CardDescription>{description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p>{content}</p>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button onClick={onAction}>{actionLabel}</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Used once... why did we create this?
|
|
<ContentCard title="..." description="..." content="..." />
|
|
```
|
|
|
|
**Problems:**
|
|
- ❌ Created before knowing if pattern is reused
|
|
- ❌ Inflexible (what if we need 2 buttons?)
|
|
- ❌ Unclear what it renders (abstraction hides structure)
|
|
- ❌ Harder to customize
|
|
|
|
---
|
|
|
|
### ✅ GOOD: Extract After 3+ Uses
|
|
|
|
```tsx
|
|
// ✅ CORRECT - After seeing pattern used 3 times, extract
|
|
function DashboardMetricCard({
|
|
title,
|
|
value,
|
|
change,
|
|
icon: Icon,
|
|
}: DashboardMetricCardProps) {
|
|
return (
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
{change && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{change > 0 ? '+' : ''}{change}% from last month
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Now used in 5+ places
|
|
<DashboardMetricCard title="Total Revenue" value="$45,231.89" change={20.1} />
|
|
<DashboardMetricCard title="Subscriptions" value="+2350" change={12.5} />
|
|
```
|
|
|
|
**Why this works:**
|
|
- ✅ Pattern validated (used 3+ times)
|
|
- ✅ Specific purpose (dashboard metrics)
|
|
- ✅ Consistent structure across uses
|
|
- ✅ Easy to update all instances
|
|
|
|
---
|
|
|
|
## Component Templates
|
|
|
|
### Template 1: Basic Custom Component
|
|
|
|
**Use case**: Simple component with optional className override
|
|
|
|
```tsx
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface MyComponentProps {
|
|
className?: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function MyComponent({ className, children }: MyComponentProps) {
|
|
return (
|
|
<div className={cn(
|
|
"base-classes-here", // Base styles
|
|
className // Allow overrides
|
|
)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<MyComponent className="custom-overrides">
|
|
Content
|
|
</MyComponent>
|
|
```
|
|
|
|
**Key points:**
|
|
- Always accept `className` prop
|
|
- Use `cn()` utility for merging
|
|
- Base classes first, overrides last
|
|
|
|
---
|
|
|
|
### Template 2: Component with Variants (CVA)
|
|
|
|
**Use case**: Component needs multiple visual variants
|
|
|
|
```tsx
|
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const componentVariants = cva(
|
|
// Base classes (always applied)
|
|
"inline-flex items-center justify-center rounded-lg font-medium transition-colors",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
},
|
|
size: {
|
|
sm: "h-8 px-3 text-xs",
|
|
default: "h-10 px-4 text-sm",
|
|
lg: "h-12 px-6 text-base",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
},
|
|
}
|
|
);
|
|
|
|
interface MyComponentProps
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
VariantProps<typeof componentVariants> {
|
|
// Additional props here
|
|
}
|
|
|
|
export function MyComponent({
|
|
variant,
|
|
size,
|
|
className,
|
|
...props
|
|
}: MyComponentProps) {
|
|
return (
|
|
<div
|
|
className={cn(componentVariants({ variant, size, className }))}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<MyComponent variant="outline" size="lg">Content</MyComponent>
|
|
```
|
|
|
|
**Key points:**
|
|
- Use CVA for complex variant logic
|
|
- Always provide `defaultVariants`
|
|
- Extend `React.HTMLAttributes` for standard HTML props
|
|
- Spread `...props` to pass through additional attributes
|
|
|
|
---
|
|
|
|
### Template 3: Composition Component
|
|
|
|
**Use case**: Wrap multiple shadcn/ui components with consistent structure
|
|
|
|
```tsx
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: string | number;
|
|
description?: string;
|
|
icon?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function StatCard({
|
|
title,
|
|
value,
|
|
description,
|
|
icon,
|
|
className,
|
|
}: StatCardProps) {
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
{icon}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
{description && (
|
|
<p className="text-xs text-muted-foreground">{description}</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<StatCard
|
|
title="Total Users"
|
|
value="1,234"
|
|
description="+12% from last month"
|
|
icon={<Users className="h-4 w-4 text-muted-foreground" />}
|
|
/>
|
|
```
|
|
|
|
**Key points:**
|
|
- Compose from shadcn/ui primitives
|
|
- Keep structure consistent
|
|
- Optional props with `?`
|
|
- Descriptive prop names
|
|
|
|
---
|
|
|
|
### Template 4: Controlled Component
|
|
|
|
**Use case**: Component manages state internally but can be controlled
|
|
|
|
```tsx
|
|
import { useState } from 'react';
|
|
|
|
interface ToggleProps {
|
|
value?: boolean;
|
|
onChange?: (value: boolean) => void;
|
|
defaultValue?: boolean;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export function Toggle({
|
|
value: controlledValue,
|
|
onChange,
|
|
defaultValue = false,
|
|
children,
|
|
}: ToggleProps) {
|
|
// Uncontrolled state
|
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
|
|
// Use controlled value if provided, otherwise use internal state
|
|
const value = controlledValue ?? uncontrolledValue;
|
|
const handleChange = (newValue: boolean) => {
|
|
if (onChange) {
|
|
onChange(newValue);
|
|
} else {
|
|
setUncontrolledValue(newValue);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<button onClick={() => handleChange(!value)}>
|
|
{value ? '✓' : '○'} {children}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// Uncontrolled usage
|
|
<Toggle defaultValue={false}>Auto-save</Toggle>
|
|
|
|
// Controlled usage
|
|
const [enabled, setEnabled] = useState(false);
|
|
<Toggle value={enabled} onChange={setEnabled}>Auto-save</Toggle>
|
|
```
|
|
|
|
**Key points:**
|
|
- Support both controlled and uncontrolled modes
|
|
- Use `defaultValue` for initial uncontrolled value
|
|
- Use `value` + `onChange` for controlled mode
|
|
- Fallback to internal state if not controlled
|
|
|
|
---
|
|
|
|
## Variant Patterns (CVA)
|
|
|
|
### What is CVA?
|
|
|
|
**class-variance-authority** (CVA) is a utility for creating component variants with Tailwind CSS.
|
|
|
|
**Why use CVA?**
|
|
- ✅ Type-safe variant props
|
|
- ✅ Compound variants (combinations)
|
|
- ✅ Default variants
|
|
- ✅ Clean, readable syntax
|
|
|
|
---
|
|
|
|
### Basic Variant Pattern
|
|
|
|
```tsx
|
|
import { cva } from 'class-variance-authority';
|
|
|
|
const alertVariants = cva(
|
|
// Base classes (always applied)
|
|
"relative w-full rounded-lg border p-4",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-background text-foreground",
|
|
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
}
|
|
);
|
|
|
|
// Usage
|
|
<div className={alertVariants({ variant: "destructive" })}>
|
|
Alert content
|
|
</div>
|
|
```
|
|
|
|
---
|
|
|
|
### Multiple Variants
|
|
|
|
```tsx
|
|
const buttonVariants = cva(
|
|
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
outline: "border border-input bg-background hover:bg-accent",
|
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
},
|
|
size: {
|
|
sm: "h-8 px-3 text-xs",
|
|
default: "h-10 px-4 text-sm",
|
|
lg: "h-12 px-6 text-base",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "default",
|
|
},
|
|
}
|
|
);
|
|
|
|
// Usage
|
|
<button className={buttonVariants({ variant: "outline", size: "lg" })}>
|
|
Large Outline Button
|
|
</button>
|
|
```
|
|
|
|
---
|
|
|
|
### Compound Variants
|
|
|
|
**Use case**: Different classes when specific variant combinations are used
|
|
|
|
```tsx
|
|
const buttonVariants = cva("base-classes", {
|
|
variants: {
|
|
variant: {
|
|
default: "bg-primary",
|
|
destructive: "bg-destructive",
|
|
},
|
|
size: {
|
|
sm: "h-8",
|
|
lg: "h-12",
|
|
},
|
|
},
|
|
// Compound variants: specific combinations
|
|
compoundVariants: [
|
|
{
|
|
variant: "destructive",
|
|
size: "lg",
|
|
class: "text-lg font-bold", // Applied when BOTH are true
|
|
},
|
|
],
|
|
defaultVariants: {
|
|
variant: "default",
|
|
size: "sm",
|
|
},
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Prop Design
|
|
|
|
### Prop Naming Conventions
|
|
|
|
**DO**:
|
|
```tsx
|
|
// ✅ Descriptive, semantic names
|
|
interface UserCardProps {
|
|
user: User;
|
|
onEdit: () => void;
|
|
isLoading: boolean;
|
|
showAvatar?: boolean;
|
|
}
|
|
```
|
|
|
|
**DON'T**:
|
|
```tsx
|
|
// ❌ Generic, unclear names
|
|
interface CardProps {
|
|
data: any;
|
|
onClick: () => void;
|
|
loading: boolean;
|
|
flag?: boolean;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Required vs Optional Props
|
|
|
|
**Guidelines:**
|
|
- Required: Core functionality depends on it
|
|
- Optional: Nice-to-have, has sensible default
|
|
|
|
```tsx
|
|
interface AlertProps {
|
|
// Required: Core to component
|
|
children: React.ReactNode;
|
|
|
|
// Optional: Has default variant
|
|
variant?: 'default' | 'destructive';
|
|
|
|
// Optional: Component works without it
|
|
onClose?: () => void;
|
|
icon?: React.ReactNode;
|
|
|
|
// Optional: Standard override
|
|
className?: string;
|
|
}
|
|
|
|
export function Alert({
|
|
children,
|
|
variant = 'default', // Default for optional prop
|
|
onClose,
|
|
icon,
|
|
className,
|
|
}: AlertProps) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Prop Type Patterns
|
|
|
|
**Enum props** (limited options):
|
|
```tsx
|
|
interface ButtonProps {
|
|
variant: 'default' | 'destructive' | 'outline';
|
|
size: 'sm' | 'default' | 'lg';
|
|
}
|
|
```
|
|
|
|
**Boolean flags**:
|
|
```tsx
|
|
interface CardProps {
|
|
isLoading?: boolean;
|
|
isDisabled?: boolean;
|
|
showBorder?: boolean;
|
|
}
|
|
```
|
|
|
|
**Callback props**:
|
|
```tsx
|
|
interface FormProps {
|
|
onSubmit: (data: FormData) => void;
|
|
onCancel?: () => void;
|
|
onChange?: (field: string, value: any) => void;
|
|
}
|
|
```
|
|
|
|
**Render props** (advanced customization):
|
|
```tsx
|
|
interface ListProps<T> {
|
|
items: T[];
|
|
renderItem: (item: T, index: number) => React.ReactNode;
|
|
renderEmpty?: () => React.ReactNode;
|
|
}
|
|
|
|
// Usage
|
|
<List
|
|
items={users}
|
|
renderItem={(user, i) => <UserCard key={i} user={user} />}
|
|
renderEmpty={() => <EmptyState />}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Checklist
|
|
|
|
Before shipping a custom component, verify:
|
|
|
|
### Visual Testing
|
|
- [ ] **Light mode** - Component looks correct
|
|
- [ ] **Dark mode** - Component looks correct (toggle theme)
|
|
- [ ] **All variants** - Test each variant works
|
|
- [ ] **Responsive** - Mobile, tablet, desktop sizes
|
|
- [ ] **Loading state** - Shows loading correctly (if applicable)
|
|
- [ ] **Error state** - Shows errors correctly (if applicable)
|
|
- [ ] **Empty state** - Handles no data gracefully
|
|
|
|
### Accessibility Testing
|
|
- [ ] **Keyboard navigation** - Can be focused and activated with Tab/Enter
|
|
- [ ] **Focus indicators** - Visible focus ring (`:focus-visible`)
|
|
- [ ] **Screen reader** - ARIA labels and roles present
|
|
- [ ] **Color contrast** - 4.5:1 for text, 3:1 for UI (use contrast checker)
|
|
- [ ] **Semantic HTML** - Using correct HTML elements (button, nav, etc.)
|
|
|
|
### Functional Testing
|
|
- [ ] **Props work** - All props apply correctly
|
|
- [ ] **className override** - Can override styles with className prop
|
|
- [ ] **Controlled/uncontrolled** - Both modes work (if applicable)
|
|
- [ ] **Event handlers** - onClick, onChange, etc. fire correctly
|
|
- [ ] **TypeScript** - No type errors, props autocomplete
|
|
|
|
### Code Quality
|
|
- [ ] **No console errors** - Check browser console
|
|
- [ ] **No warnings** - React warnings, a11y warnings
|
|
- [ ] **Performance** - No unnecessary re-renders
|
|
- [ ] **Documentation** - JSDoc comments for complex props
|
|
|
|
---
|
|
|
|
## Real-World Examples
|
|
|
|
### Example 1: Stat Card
|
|
|
|
**Problem**: Dashboard shows 8 metric cards with same structure.
|
|
|
|
**Solution**: Extract composition after 3rd use.
|
|
|
|
```tsx
|
|
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
|
import { cn } from '@/lib/utils';
|
|
import { LucideIcon } from 'lucide-react';
|
|
|
|
interface StatCardProps {
|
|
title: string;
|
|
value: string | number;
|
|
change?: number;
|
|
icon?: LucideIcon;
|
|
className?: string;
|
|
}
|
|
|
|
export function StatCard({
|
|
title,
|
|
value,
|
|
change,
|
|
icon: Icon,
|
|
className,
|
|
}: StatCardProps) {
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{value}</div>
|
|
{change !== undefined && (
|
|
<p className={cn(
|
|
"text-xs",
|
|
change >= 0 ? "text-green-600" : "text-destructive"
|
|
)}>
|
|
{change >= 0 ? '+' : ''}{change}% from last month
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
<StatCard title="Total Revenue" value="$45,231.89" change={20.1} icon={DollarSign} />
|
|
<StatCard title="Subscriptions" value="+2350" change={12.5} icon={Users} />
|
|
<StatCard title="Sales" value="+12,234" change={19} icon={CreditCard} />
|
|
<StatCard title="Active Now" value="+573" change={-2.1} icon={Activity} />
|
|
</div>
|
|
```
|
|
|
|
**Why this works:**
|
|
- Specific purpose (dashboard metrics)
|
|
- Reused 8+ times
|
|
- Consistent structure
|
|
- Easy to update all instances
|
|
|
|
---
|
|
|
|
### Example 2: Confirmation Dialog
|
|
|
|
**Problem**: Need to confirm delete actions throughout app.
|
|
|
|
**Solution**: Create reusable confirmation dialog wrapper.
|
|
|
|
```tsx
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface ConfirmDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
title: string;
|
|
description: string;
|
|
confirmLabel?: string;
|
|
cancelLabel?: string;
|
|
variant?: 'default' | 'destructive';
|
|
onConfirm: () => void | Promise<void>;
|
|
}
|
|
|
|
export function ConfirmDialog({
|
|
open,
|
|
onOpenChange,
|
|
title,
|
|
description,
|
|
confirmLabel = 'Confirm',
|
|
cancelLabel = 'Cancel',
|
|
variant = 'destructive',
|
|
onConfirm,
|
|
}: ConfirmDialogProps) {
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const handleConfirm = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
await onConfirm();
|
|
onOpenChange(false);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{title}</DialogTitle>
|
|
<DialogDescription>{description}</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
disabled={isLoading}
|
|
>
|
|
{cancelLabel}
|
|
</Button>
|
|
<Button
|
|
variant={variant}
|
|
onClick={handleConfirm}
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? 'Processing...' : confirmLabel}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
|
|
<ConfirmDialog
|
|
open={showDeleteDialog}
|
|
onOpenChange={setShowDeleteDialog}
|
|
title="Delete User"
|
|
description="Are you sure you want to delete this user? This action cannot be undone."
|
|
confirmLabel="Delete"
|
|
variant="destructive"
|
|
onConfirm={async () => {
|
|
await deleteUser(user.id);
|
|
toast.success('User deleted');
|
|
}}
|
|
/>
|
|
```
|
|
|
|
**Why this works:**
|
|
- Common pattern (confirmations)
|
|
- Handles loading states automatically
|
|
- Consistent UX across app
|
|
- Easy to use
|
|
|
|
---
|
|
|
|
### Example 3: Page Header
|
|
|
|
**Problem**: Every page has header with title, description, and optional action.
|
|
|
|
**Solution**: Extract page header component.
|
|
|
|
```tsx
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface PageHeaderProps {
|
|
title: string;
|
|
description?: string;
|
|
action?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function PageHeader({
|
|
title,
|
|
description,
|
|
action,
|
|
className,
|
|
}: PageHeaderProps) {
|
|
return (
|
|
<div className={cn("flex items-center justify-between", className)}>
|
|
<div className="space-y-1">
|
|
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
|
|
{description && (
|
|
<p className="text-muted-foreground">{description}</p>
|
|
)}
|
|
</div>
|
|
{action && <div>{action}</div>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Usage
|
|
<PageHeader
|
|
title="Users"
|
|
description="Manage system users and permissions"
|
|
action={
|
|
<Button onClick={() => router.push('/users/new')}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create User
|
|
</Button>
|
|
}
|
|
/>
|
|
```
|
|
|
|
---
|
|
|
|
## Summary: Component Creation Checklist
|
|
|
|
Before creating a custom component, ask:
|
|
|
|
- [ ] **Is it reused 3+ times?** If no, compose inline
|
|
- [ ] **Does shadcn/ui have this?** If yes, use it
|
|
- [ ] **Can I compose existing components?** If yes, do that first
|
|
- [ ] **Does it need variants?** Use CVA
|
|
- [ ] **Is className supported?** Always allow overrides
|
|
- [ ] **Is it accessible?** Test keyboard, screen reader, contrast
|
|
- [ ] **Is it documented?** Add JSDoc comments
|
|
- [ ] **Does it follow conventions?** Match shadcn/ui patterns
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
- **Practice**: Refactor inline compositions into components after 3+ uses
|
|
- **Explore**: [Component showcase](/dev/components)
|
|
- **Reference**: [shadcn/ui source code](https://github.com/shadcn-ui/ui/tree/main/apps/www/registry)
|
|
|
|
---
|
|
|
|
**Related Documentation:**
|
|
- [Components](./02-components.md) - shadcn/ui component library
|
|
- [AI Guidelines](./08-ai-guidelines.md) - Component templates for AI
|
|
- [Forms](./06-forms.md) - Form component patterns
|
|
- [Accessibility](./07-accessibility.md) - Accessibility requirements
|
|
|
|
**Last Updated**: November 2, 2025
|