Add documentation for component creation and design system structure

- **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.
This commit is contained in:
2025-11-02 12:32:01 +01:00
parent 13f830ed6d
commit 30cbaf8ad5
13 changed files with 7780 additions and 1447 deletions

View File

@@ -0,0 +1,456 @@
# Quick Start Guide
Get up and running with the FastNext design system immediately. This guide covers the essential patterns you need to build 80% of interfaces.
---
## TL;DR
```tsx
// 1. Import from @/components/ui/*
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
// 2. Use semantic color tokens
className="bg-primary text-primary-foreground"
className="text-destructive"
// 3. Use spacing scale (4, 8, 12, 16, 24, 32...)
className="p-4 space-y-6"
// 4. Build layouts with these patterns
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Your content */}
</div>
</div>
```
---
## 1. Essential Components
### Buttons
```tsx
import { Button } from '@/components/ui/button';
// Primary action
<Button>Save Changes</Button>
// Danger action
<Button variant="destructive">Delete</Button>
// Secondary action
<Button variant="outline">Cancel</Button>
// Subtle action
<Button variant="ghost">Skip</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
```
**[See all button variants](/dev/components#button)**
---
### Cards
```tsx
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter
} from '@/components/ui/card';
<Card>
<CardHeader>
<CardTitle>User Profile</CardTitle>
<CardDescription>Manage your account settings</CardDescription>
</CardHeader>
<CardContent>
<p>Card content goes here</p>
</CardContent>
<CardFooter>
<Button>Save</Button>
</CardFooter>
</Card>
```
**[See card examples](/dev/components#card)**
---
### Forms
```tsx
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
{...register('email')}
/>
{errors.email && (
<p className="text-sm text-destructive">
{errors.email.message}
</p>
)}
</div>
```
**[See form patterns](./06-forms.md)** | **[Form examples](/dev/forms)**
---
### Dialogs/Modals
```tsx
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
DialogTrigger
} from '@/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>
Are you sure you want to proceed?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
```
**[See dialog examples](/dev/components#dialog)**
---
### Alerts
```tsx
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
// Default alert
<Alert>
<AlertCircle 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>
```
**[See all component variants](/dev/components)**
---
## 2. Essential Layouts (1 minute)
### Page Container
```tsx
// Standard page layout
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">Page Title</h1>
<Card>{/* Content */}</Card>
</div>
</div>
```
### Dashboard Grid
```tsx
// Responsive card grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map(item => (
<Card key={item.id}>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
</CardHeader>
<CardContent>{item.content}</CardContent>
</Card>
))}
</div>
```
### Form Layout
```tsx
// Centered form with max width
<div className="container mx-auto px-4 py-8">
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Login</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
{/* Form fields */}
</form>
</CardContent>
</Card>
</div>
```
**[See all layout patterns](./03-layouts.md)** | **[Layout examples](/dev/layouts)**
---
## 3. Color System
**Always use semantic tokens**, never arbitrary colors:
```tsx
// ✅ GOOD - Semantic tokens
<div className="bg-primary text-primary-foreground">Primary</div>
<div className="bg-destructive text-destructive-foreground">Error</div>
<div className="bg-muted text-muted-foreground">Disabled</div>
<p className="text-foreground">Body text</p>
<p className="text-muted-foreground">Secondary text</p>
// ❌ BAD - Arbitrary colors
<div className="bg-blue-500 text-white">Don't do this</div>
```
**Available tokens:**
- `primary` - Main brand color, CTAs
- `destructive` - Errors, delete actions
- `muted` - Disabled states, subtle backgrounds
- `accent` - Hover states, highlights
- `foreground` - Body text
- `muted-foreground` - Secondary text
- `border` - Borders, dividers
**[See complete color system](./01-foundations.md#color-system-oklch)**
---
## 4. Spacing System
**Use multiples of 4** (Tailwind's base unit is 0.25rem = 4px):
```tsx
// ✅ GOOD - Consistent spacing
<div className="p-4 space-y-6">
<div className="mb-8">Content</div>
</div>
// ❌ BAD - Arbitrary spacing
<div className="p-[13px] space-y-[17px]">
<div className="mb-[23px]">Content</div>
</div>
```
**Common spacing values:**
- `2` (8px) - Tight spacing
- `4` (16px) - Standard spacing
- `6` (24px) - Section spacing
- `8` (32px) - Large gaps
- `12` (48px) - Section dividers
**Pro tip:** Use `gap-` for grids/flex, `space-y-` for vertical stacks:
```tsx
// Grid spacing
<div className="grid grid-cols-3 gap-6">
// Stack spacing
<div className="space-y-4">
```
**[Read spacing philosophy](./04-spacing-philosophy.md)**
---
## 5. Responsive Design
**Mobile-first approach** with Tailwind breakpoints:
```tsx
<div className="
p-4 // Mobile: 16px padding
sm:p-6 // Tablet: 24px padding
lg:p-8 // Desktop: 32px padding
">
<h1 className="
text-2xl // Mobile: 24px
sm:text-3xl // Tablet: 30px
lg:text-4xl // Desktop: 36px
font-bold
">
Responsive Title
</h1>
</div>
// Grid columns
<div className="grid
grid-cols-1 // Mobile: 1 column
md:grid-cols-2 // Tablet: 2 columns
lg:grid-cols-3 // Desktop: 3 columns
gap-6
">
```
**Breakpoints:**
- `sm:` 640px+
- `md:` 768px+
- `lg:` 1024px+
- `xl:` 1280px+
---
## 6. Accessibility
**Always include:**
```tsx
// Labels for inputs
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
// ARIA for errors
<Input
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="text-sm text-destructive">
{errors.email.message}
</p>
)}
// ARIA labels for icon-only buttons
<Button size="icon" aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
```
**[Complete accessibility guide](./07-accessibility.md)**
---
## 7. Common Patterns Cheat Sheet
### Loading State
```tsx
import { Skeleton } from '@/components/ui/skeleton';
{isLoading ? (
<Skeleton className="h-12 w-full" />
) : (
<div>{content}</div>
)}
```
### Dropdown Menu
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">Options</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
```
### Badge/Tag
```tsx
import { Badge } from '@/components/ui/badge';
<Badge>New</Badge>
<Badge variant="destructive">Urgent</Badge>
<Badge variant="outline">Draft</Badge>
```
---
## 8. Next Steps
You now know enough to build most interfaces! For deeper knowledge:
### Learn More
- **Components**: [Complete component guide](./02-components.md)
- **Layouts**: [Layout patterns](./03-layouts.md)
- **Forms**: [Form patterns & validation](./06-forms.md)
- **Custom Components**: [Component creation guide](./05-component-creation.md)
### Interactive Examples
- **[Component Showcase](/dev/components)** - All components with code
- **[Layout Examples](/dev/layouts)** - Before/after comparisons
- **[Form Examples](/dev/forms)** - Complete form implementations
### Reference
- **[Quick Reference Tables](./99-reference.md)** - Bookmark this for lookups
- **[Foundations](./01-foundations.md)** - Complete color/spacing/typography guide
---
## 🎯 Golden Rules
Remember these and you'll be 95% compliant:
1.**Import from `@/components/ui/*`**
2.**Use semantic colors**: `bg-primary`, not `bg-blue-500`
3.**Use spacing scale**: 4, 8, 12, 16, 24, 32 (multiples of 4)
4.**Use `cn()` for className merging**: `cn("base", conditional && "extra", className)`
5.**Add accessibility**: Labels, ARIA, keyboard support
6.**Test in dark mode**: Toggle with theme switcher
---
## 🚀 Start Building!
You're ready to build. When you hit edge cases or need advanced patterns, refer back to the [full documentation](./README.md).
**Bookmark these:**
- [Quick Reference](./99-reference.md) - For quick lookups
- [AI Guidelines](./08-ai-guidelines.md) - If using AI assistants
- [Component Showcase](/dev/components) - For copy-paste examples
Happy coding! 🎨

View File

@@ -0,0 +1,909 @@
# Foundations
**The building blocks of our design system**: OKLCH colors, typography scale, spacing tokens, shadows, and border radius. Master these fundamentals to build consistent, accessible interfaces.
---
## Table of Contents
1. [Technology Stack](#technology-stack)
2. [Color System (OKLCH)](#color-system-oklch)
3. [Typography](#typography)
4. [Spacing Scale](#spacing-scale)
5. [Shadows](#shadows)
6. [Border Radius](#border-radius)
7. [Quick Reference](#quick-reference)
---
## Technology Stack
### Core Technologies
- **Framework**: Next.js 15 + React 19
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
- **Components**: shadcn/ui (New York style)
- **Color Space**: OKLCH (perceptually uniform)
- **Icons**: lucide-react
- **Fonts**: Geist Sans + Geist Mono
### Design Principles
1. **🎨 Semantic First** - Use `bg-primary`, not `bg-blue-500`
2. **♿ Accessible by Default** - WCAG AA compliance minimum (4.5:1 contrast)
3. **📐 Consistent Spacing** - Multiples of 4px (0.25rem base unit)
4. **🧩 Compose, Don't Create** - Use shadcn/ui primitives
5. **🌗 Dark Mode Ready** - All components work in light/dark
6. **⚡ Pareto Efficient** - 80% of needs with 20% of patterns
---
## Color System (OKLCH)
### Why OKLCH?
We use **OKLCH** (Oklab LCH) color space for:
-**Perceptual uniformity** - Colors look consistent across light/dark modes
-**Better accessibility** - Predictable contrast ratios
-**Vibrant colors** - More saturated without sacrificing legibility
-**Future-proof** - CSS native support (vs HSL/RGB)
**Learn more**: [oklch.com](https://oklch.com)
---
### Semantic Color Tokens
All colors follow the **background/foreground** convention:
- `background` - The background color
- `foreground` - The text color that goes on that background
**This ensures accessible contrast automatically.**
---
### Primary Colors
**Purpose**: Main brand color, CTAs, primary actions
```css
/* Light & Dark Mode */
--primary: oklch(0.6231 0.1880 259.8145) /* Blue */
--primary-foreground: oklch(1 0 0) /* White text */
```
**Usage**:
```tsx
// Primary button (most common)
<Button>Save Changes</Button>
// Primary link
<a href="#" className="text-primary hover:underline">
Learn more
</a>
// Primary badge
<Badge className="bg-primary text-primary-foreground">New</Badge>
```
**When to use**:
- ✅ Call-to-action buttons
- ✅ Primary links
- ✅ Active states in navigation
- ✅ Important badges/tags
**When NOT to use**:
- ❌ Large background areas (too intense)
- ❌ Body text (use `text-foreground`)
- ❌ Disabled states (use `muted`)
---
### Secondary Colors
**Purpose**: Secondary actions, less prominent UI elements
```css
/* Light Mode */
--secondary: oklch(0.9670 0.0029 264.5419) /* Light gray-blue */
--secondary-foreground: oklch(0.1529 0 0) /* Dark text */
/* Dark Mode */
--secondary: oklch(0.2686 0 0) /* Dark gray */
--secondary-foreground: oklch(0.9823 0 0) /* Light text */
```
**Usage**:
```tsx
// Secondary button
<Button variant="secondary">Cancel</Button>
// Secondary badge
<Badge variant="secondary">Draft</Badge>
// Muted background area
<div className="bg-secondary text-secondary-foreground p-4 rounded-lg">
Less important information
</div>
```
---
### Muted Colors
**Purpose**: Backgrounds for disabled states, subtle UI elements
```css
/* Light Mode */
--muted: oklch(0.9846 0.0017 247.8389)
--muted-foreground: oklch(0.4667 0.0043 264.4327)
/* Dark Mode */
--muted: oklch(0.2393 0 0)
--muted-foreground: oklch(0.6588 0.0043 264.4327)
```
**Usage**:
```tsx
// Disabled button
<Button disabled>Submit</Button>
// Secondary/helper text
<p className="text-muted-foreground text-sm">
This action cannot be undone
</p>
// Skeleton loader
<Skeleton className="h-12 w-full" />
// TabsList background
<Tabs defaultValue="tab1">
<TabsList className="bg-muted">
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
</TabsList>
</Tabs>
```
**Common use cases**:
- Disabled button backgrounds
- Placeholder/skeleton loaders
- TabsList backgrounds
- Switch backgrounds (unchecked state)
- Helper text, captions, timestamps
---
### Accent Colors
**Purpose**: Hover states, focus indicators, highlights
```css
/* Light Mode */
--accent: oklch(0.9514 0.0250 236.8242)
--accent-foreground: oklch(0.1529 0 0)
/* Dark Mode */
--accent: oklch(0.3791 0.1378 265.5222)
--accent-foreground: oklch(0.9823 0 0)
```
**Usage**:
```tsx
// Dropdown menu item hover
<DropdownMenu>
<DropdownMenuContent>
<DropdownMenuItem className="focus:bg-accent focus:text-accent-foreground">
Edit
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
// Highlighted section
<div className="bg-accent text-accent-foreground p-4 rounded-lg">
Featured content
</div>
```
**Common use cases**:
- Dropdown menu item hover states
- Command palette hover states
- Highlighted sections
- Subtle emphasis backgrounds
---
### Destructive Colors
**Purpose**: Error states, delete actions, warnings
```css
/* Light & Dark Mode */
--destructive: oklch(0.6368 0.2078 25.3313) /* Red */
--destructive-foreground: oklch(1 0 0) /* White text */
```
**Usage**:
```tsx
// Delete button
<Button variant="destructive">Delete Account</Button>
// Error alert
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
// Form error text
<p className="text-sm text-destructive">
{errors.email?.message}
</p>
// Destructive badge
<Badge variant="destructive">Critical</Badge>
```
**When to use**:
- ✅ Delete/remove actions
- ✅ Error messages
- ✅ Validation errors
- ✅ Critical warnings
---
### Card & Popover Colors
**Purpose**: Elevated surfaces (cards, popovers, dropdowns)
```css
/* Light Mode */
--card: oklch(1.0000 0 0) /* White */
--card-foreground: oklch(0.1529 0 0) /* Dark text */
--popover: oklch(1.0000 0 0) /* White */
--popover-foreground: oklch(0.1529 0 0) /* Dark text */
/* Dark Mode */
--card: oklch(0.2686 0 0) /* Dark gray */
--card-foreground: oklch(0.9823 0 0) /* Light text */
--popover: oklch(0.2686 0 0) /* Dark gray */
--popover-foreground: oklch(0.9823 0 0) /* Light text */
```
**Usage**:
```tsx
// Card (uses card colors by default)
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
</CardHeader>
<CardContent>Card content</CardContent>
</Card>
// Popover
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>Popover content</PopoverContent>
</Popover>
```
---
### Border & Input Colors
**Purpose**: Borders, input field borders, dividers
```css
/* Light Mode */
--border: oklch(0.9276 0.0058 264.5313)
--input: oklch(0.9276 0.0058 264.5313)
/* Dark Mode */
--border: oklch(0.3715 0 0)
--input: oklch(0.3715 0 0)
```
**Usage**:
```tsx
// Input border
<Input type="email" placeholder="you@example.com" />
// Card with border
<Card className="border">Content</Card>
// Separator
<Separator />
// Custom border
<div className="border-border border-2 rounded-lg p-4">
Content
</div>
```
---
### Focus Ring
**Purpose**: Focus indicators for keyboard navigation
```css
/* Light & Dark Mode */
--ring: oklch(0.6231 0.1880 259.8145) /* Primary blue */
```
**Usage**:
```tsx
// Button with focus ring (automatic)
<Button>Click me</Button>
// Custom focusable element
<div
tabIndex={0}
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Focusable content
</div>
```
**Accessibility note**: Focus rings are critical for keyboard navigation. Never remove them with `outline: none` without providing an alternative.
---
### Chart Colors
**Purpose**: Data visualization with harmonious color palette
```css
--chart-1: oklch(0.6231 0.1880 259.8145) /* Blue */
--chart-2: oklch(0.5461 0.2152 262.8809) /* Purple-blue */
--chart-3: oklch(0.4882 0.2172 264.3763) /* Deep purple */
--chart-4: oklch(0.4244 0.1809 265.6377) /* Violet */
--chart-5: oklch(0.3791 0.1378 265.5222) /* Deep violet */
```
**Usage**:
```tsx
// In chart components
const COLORS = [
'hsl(var(--chart-1))',
'hsl(var(--chart-2))',
'hsl(var(--chart-3))',
'hsl(var(--chart-4))',
'hsl(var(--chart-5))',
];
```
---
### Color Decision Tree
```
What's the purpose?
├─ Main action/CTA? → PRIMARY
├─ Secondary action? → SECONDARY
├─ Error/delete? → DESTRUCTIVE
├─ Hover state? → ACCENT
├─ Disabled/subtle? → MUTED
├─ Card/elevated surface? → CARD
├─ Border/divider? → BORDER
└─ Focus indicator? → RING
```
---
### Color Usage Guidelines
#### ✅ DO
```tsx
// Use semantic tokens
<div className="bg-primary text-primary-foreground">CTA</div>
<p className="text-destructive">Error message</p>
<div className="bg-muted text-muted-foreground">Subtle background</div>
// Use accent for hover
<div className="hover:bg-accent hover:text-accent-foreground">
Hover me
</div>
// Test contrast
// Primary on white: 4.5:1 ✅
// Destructive on white: 4.5:1 ✅
```
#### ❌ DON'T
```tsx
// Don't use arbitrary colors
<div className="bg-blue-500 text-white">Bad</div>
// Don't mix color spaces
<div className="bg-primary text-[#ff0000]">Bad</div>
// Don't use primary for large areas
<div className="min-h-screen bg-primary">Too intense</div>
// Don't override foreground without checking contrast
<div className="bg-primary text-gray-300">Low contrast!</div>
```
---
## Typography
### Font Families
```css
--font-sans: Geist Sans, system-ui, -apple-system, sans-serif
--font-mono: Geist Mono, ui-monospace, monospace
--font-serif: ui-serif, Georgia, serif
```
**Usage**:
```tsx
// Sans serif (default)
<div className="font-sans">Body text</div>
// Monospace (code)
<code className="font-mono">const example = true;</code>
// Serif (rarely used)
<blockquote className="font-serif italic">Quote</blockquote>
```
---
### Type Scale
| Size | Class | rem | px | Use Case |
|------|-------|-----|----|----|
| 9xl | `text-9xl` | 8rem | 128px | Hero text (rare) |
| 8xl | `text-8xl` | 6rem | 96px | Hero text (rare) |
| 7xl | `text-7xl` | 4.5rem | 72px | Hero text (rare) |
| 6xl | `text-6xl` | 3.75rem | 60px | Hero text (rare) |
| 5xl | `text-5xl` | 3rem | 48px | Landing page H1 |
| 4xl | `text-4xl` | 2.25rem | 36px | Page H1 |
| 3xl | `text-3xl` | 1.875rem | 30px | **Page titles** |
| 2xl | `text-2xl` | 1.5rem | 24px | **Section headings** |
| xl | `text-xl` | 1.25rem | 20px | **Card titles** |
| lg | `text-lg` | 1.125rem | 18px | **Subheadings** |
| base | `text-base` | 1rem | 16px | **Body text (default)** |
| sm | `text-sm` | 0.875rem | 14px | **Secondary text, captions** |
| xs | `text-xs` | 0.75rem | 12px | **Labels, helper text** |
**Bold = most commonly used**
---
### Font Weights
| Weight | Class | Numeric | Use Case |
|--------|-------|---------|----------|
| Bold | `font-bold` | 700 | **Headings, emphasis** |
| Semibold | `font-semibold` | 600 | **Subheadings, buttons** |
| Medium | `font-medium` | 500 | **Labels, menu items** |
| Normal | `font-normal` | 400 | **Body text (default)** |
| Light | `font-light` | 300 | De-emphasized text |
**Bold = most commonly used**
---
### Typography Patterns
#### Page Title
```tsx
<h1 className="text-3xl font-bold">Page Title</h1>
```
#### Section Heading
```tsx
<h2 className="text-2xl font-semibold mb-4">Section Heading</h2>
```
#### Card Title
```tsx
<CardTitle className="text-xl font-semibold">Card Title</CardTitle>
```
#### Body Text
```tsx
<p className="text-base text-foreground">
Regular paragraph text uses the default text-base size.
</p>
```
#### Secondary Text
```tsx
<p className="text-sm text-muted-foreground">
Helper text, timestamps, captions
</p>
```
#### Label
```tsx
<Label htmlFor="email" className="text-sm font-medium">
Email Address
</Label>
```
---
### Line Height
| Class | Value | Use Case |
|-------|-------|----------|
| `leading-none` | 1 | Headings (rare) |
| `leading-tight` | 1.25 | **Headings** |
| `leading-snug` | 1.375 | Dense text |
| `leading-normal` | 1.5 | **Body text (default)** |
| `leading-relaxed` | 1.625 | Comfortable reading |
| `leading-loose` | 2 | Very relaxed (rare) |
**Usage**:
```tsx
// Heading
<h1 className="text-3xl font-bold leading-tight">
Tight line height for headings
</h1>
// Body (default)
<p className="leading-normal">
Normal line height for readability
</p>
```
---
### Typography Guidelines
#### ✅ DO
```tsx
// Use semantic foreground colors
<p className="text-foreground">Body text</p>
<p className="text-muted-foreground">Secondary text</p>
// Maintain heading hierarchy
<h1 className="text-3xl font-bold">Page Title</h1>
<h2 className="text-2xl font-semibold">Section</h2>
<h3 className="text-xl font-semibold">Subsection</h3>
// Limit line length for readability
<article className="max-w-2xl mx-auto">
<p>60-80 characters per line is optimal</p>
</article>
// Use responsive type sizes
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
Responsive Title
</h1>
```
#### ❌ DON'T
```tsx
// Don't use too many sizes on one page
<p className="text-xs">Too small</p>
<p className="text-sm">Still small</p>
<p className="text-base">Base</p>
<p className="text-lg">Large</p>
<p className="text-xl">Larger</p>
// ^ Pick 2-3 sizes max
// Don't skip heading levels
<h1>Page</h1>
<h3>Section</h3> // ❌ Skipped h2
// Don't use custom colors without contrast check
<p className="text-blue-300">Low contrast</p>
// Don't overuse bold
<p className="font-bold">
<span className="font-bold">Every</span>
<span className="font-bold">word</span>
<span className="font-bold">bold</span>
</p>
```
---
## Spacing Scale
Tailwind uses a **0.25rem (4px) base unit**:
```css
--spacing: 0.25rem;
```
**All spacing should be multiples of 4px** for consistency.
### Spacing Tokens
| Token | rem | Pixels | Use Case |
|-------|-----|--------|----------|
| `0` | 0 | 0px | No spacing |
| `px` | - | 1px | Borders, dividers |
| `0.5` | 0.125rem | 2px | Very tight |
| `1` | 0.25rem | 4px | Icon gaps |
| `2` | 0.5rem | 8px | **Tight spacing** (label → input) |
| `3` | 0.75rem | 12px | Component padding |
| `4` | 1rem | 16px | **Standard spacing** (form fields) |
| `5` | 1.25rem | 20px | Medium spacing |
| `6` | 1.5rem | 24px | **Section spacing** (cards) |
| `8` | 2rem | 32px | **Large gaps** |
| `10` | 2.5rem | 40px | Very large gaps |
| `12` | 3rem | 48px | **Section dividers** |
| `16` | 4rem | 64px | **Page sections** |
| `20` | 5rem | 80px | Extra large |
| `24` | 6rem | 96px | Huge spacing |
**Bold = most commonly used**
---
### Container & Max Width
```tsx
// Responsive container with horizontal padding
<div className="container mx-auto px-4">
Content
</div>
// Constrained width for readability
<div className="max-w-2xl mx-auto">
Article content
</div>
```
### Max Width Scale
| Class | Pixels | Use Case |
|-------|--------|----------|
| `max-w-xs` | 320px | Tiny cards |
| `max-w-sm` | 384px | Small cards |
| `max-w-md` | 448px | **Forms** |
| `max-w-lg` | 512px | **Modals** |
| `max-w-xl` | 576px | Medium content |
| `max-w-2xl` | 672px | **Article content** |
| `max-w-3xl` | 768px | Documentation |
| `max-w-4xl` | 896px | **Wide layouts** |
| `max-w-5xl` | 1024px | Extra wide |
| `max-w-6xl` | 1152px | Very wide |
| `max-w-7xl` | 1280px | **Full page width** |
**Bold = most commonly used**
---
### Spacing Guidelines
#### ✅ DO
```tsx
// Use multiples of 4
<div className="p-4 space-y-6 mb-8">Content</div>
// Use gap for flex/grid
<div className="flex gap-4">
<Button>Cancel</Button>
<Button>Save</Button>
</div>
// Use space-y for stacks
<form className="space-y-4">
<Input />
<Input />
</form>
// Use responsive spacing
<div className="p-4 sm:p-6 lg:p-8">
Responsive padding
</div>
```
#### ❌ DON'T
```tsx
// Don't use arbitrary values
<div className="p-[13px] mb-[17px]">Bad</div>
// Don't mix methods inconsistently
<div className="space-y-4">
<div className="mb-2">Inconsistent</div>
<div className="mb-6">Inconsistent</div>
</div>
// Don't forget responsive spacing
<div className="p-8">Too much padding on mobile</div>
```
**See [Spacing Philosophy](./04-spacing-philosophy.md) for detailed spacing strategy.**
---
## Shadows
Professional shadow system for depth and elevation:
```css
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05)
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10)
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10)
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10)
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10)
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25)
```
### Shadow Usage
| Elevation | Class | Use Case |
|-----------|-------|----------|
| Base | No shadow | Buttons, inline elements |
| Low | `shadow-sm` | **Cards, panels** |
| Medium | `shadow-md` | **Dropdowns, tooltips** |
| High | `shadow-lg` | **Modals, popovers** |
| Highest | `shadow-xl` | Notifications, floating elements |
| Maximum | `shadow-2xl` | Dialogs (rare) |
**Usage**:
```tsx
// Card with subtle shadow
<Card className="shadow-sm">Card content</Card>
// Dropdown with medium shadow
<DropdownMenuContent className="shadow-md">
Menu items
</DropdownMenuContent>
// Modal with high shadow
<DialogContent className="shadow-lg">
Modal content
</DialogContent>
// Floating notification
<div className="fixed top-4 right-4 shadow-xl rounded-lg p-4">
Notification
</div>
```
**Dark mode note**: Shadows are less visible in dark mode. Test both modes.
---
## Border Radius
Consistent rounded corners across the application:
```css
--radius: 0.375rem; /* 6px - base */
--radius-sm: calc(var(--radius) - 4px) /* 2px */
--radius-md: calc(var(--radius) - 2px) /* 4px */
--radius-lg: var(--radius) /* 6px */
--radius-xl: calc(var(--radius) + 4px) /* 10px */
```
### Border Radius Scale
| Token | Class | Pixels | Use Case |
|-------|-------|--------|----------|
| None | `rounded-none` | 0px | Square elements |
| Small | `rounded-sm` | 2px | **Tags, small badges** |
| Medium | `rounded-md` | 4px | **Inputs, small buttons** |
| Large | `rounded-lg` | 6px | **Cards, buttons (default)** |
| XL | `rounded-xl` | 10px | **Large cards, modals** |
| 2XL | `rounded-2xl` | 16px | Hero sections |
| 3XL | `rounded-3xl` | 24px | Very rounded |
| Full | `rounded-full` | 9999px | **Pills, avatars, icon buttons** |
**Bold = most commonly used**
### Usage Examples
```tsx
// Button (default)
<Button className="rounded-lg">Default Button</Button>
// Input field
<Input className="rounded-md" />
// Card
<Card className="rounded-xl">Large card</Card>
// Avatar
<Avatar className="rounded-full">
<AvatarImage src="/avatar.jpg" />
</Avatar>
// Badge/Tag
<Badge className="rounded-sm">Small tag</Badge>
// Pill button
<Button className="rounded-full">Pill Button</Button>
```
### Directional Radius
```tsx
// Top corners only
<div className="rounded-t-lg">Top rounded</div>
// Bottom corners only
<div className="rounded-b-lg">Bottom rounded</div>
// Left corners only
<div className="rounded-l-lg">Left rounded</div>
// Right corners only
<div className="rounded-r-lg">Right rounded</div>
// Individual corners
<div className="rounded-tl-lg rounded-br-lg">
Top-left and bottom-right
</div>
```
---
## Quick Reference
### Most Used Tokens
**Colors**:
- `bg-primary text-primary-foreground` - CTAs
- `bg-destructive text-destructive-foreground` - Delete/errors
- `bg-muted text-muted-foreground` - Disabled/subtle
- `text-foreground` - Body text
- `text-muted-foreground` - Secondary text
- `border-border` - Borders
**Typography**:
- `text-3xl font-bold` - Page titles
- `text-2xl font-semibold` - Section headings
- `text-xl font-semibold` - Card titles
- `text-base` - Body text
- `text-sm text-muted-foreground` - Secondary text
**Spacing**:
- `p-4` - Standard padding (16px)
- `p-6` - Card padding (24px)
- `gap-4` - Standard gap (16px)
- `gap-6` - Section gap (24px)
- `space-y-4` - Form field spacing (16px)
- `space-y-6` - Section spacing (24px)
**Shadows & Radius**:
- `shadow-sm` - Cards
- `shadow-md` - Dropdowns
- `shadow-lg` - Modals
- `rounded-lg` - Buttons, cards (6px)
- `rounded-full` - Avatars, pills
---
## Next Steps
- **Quick Start**: [5-minute crash course](./00-quick-start.md)
- **Components**: [shadcn/ui component guide](./02-components.md)
- **Layouts**: [Layout patterns](./03-layouts.md)
- **Spacing**: [Spacing philosophy](./04-spacing-philosophy.md)
- **Reference**: [Quick lookup tables](./99-reference.md)
---
**Related Documentation:**
- [Quick Start](./00-quick-start.md) - Essential patterns
- [Components](./02-components.md) - shadcn/ui library
- [Spacing Philosophy](./04-spacing-philosophy.md) - Margin vs padding strategy
- [Accessibility](./07-accessibility.md) - WCAG compliance
**External Resources:**
- [OKLCH Color Picker](https://oklch.com)
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
**Last Updated**: November 2, 2025

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,586 @@
# Layout Patterns
**Master the 5 essential layouts** that cover 80% of all interface needs. Learn when to use Grid vs Flex, and build responsive, consistent layouts every time.
---
## Table of Contents
1. [Grid vs Flex Decision Tree](#grid-vs-flex-decision-tree)
2. [The 5 Essential Patterns](#the-5-essential-patterns)
3. [Responsive Strategies](#responsive-strategies)
4. [Common Mistakes](#common-mistakes)
5. [Advanced Patterns](#advanced-patterns)
---
## Grid vs Flex Decision Tree
Use this flowchart to choose between Grid and Flex:
```
┌─────────────────────────────────────┐
│ Need equal-width columns? │
│ (e.g., 3 cards of same width) │
└──────────┬─YES──────────┬─NO────────┘
│ │
▼ ▼
USE GRID Need 2D layout?
(rows + columns)
┌────┴────┐
│YES │NO
▼ ▼
USE GRID USE FLEX
```
### Quick Rules
| Scenario | Solution |
|----------|----------|
| **Equal-width columns** | Grid (`grid grid-cols-3`) |
| **Flexible item sizes** | Flex (`flex gap-4`) |
| **2D layout (rows + cols)** | Grid (`grid grid-cols-2 grid-rows-3`) |
| **1D layout (row OR col)** | Flex (`flex` or `flex flex-col`) |
| **Card grid** | Grid (`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3`) |
| **Navbar items** | Flex (`flex items-center gap-4`) |
| **Sidebar + Content** | Flex (`flex gap-6`) |
| **Form fields** | Flex column (`flex flex-col gap-4` or `space-y-4`) |
---
## The 5 Essential Patterns
These 5 patterns cover 80% of all layout needs. Master these first.
---
### 1. Page Container Pattern
**Use case**: Standard page layout with readable content width
```tsx
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">Page Title</h1>
<Card>
<CardHeader>
<CardTitle>Section Title</CardTitle>
</CardHeader>
<CardContent>
Page content goes here
</CardContent>
</Card>
</div>
</div>
```
**Key Features:**
- `container` - Responsive container with max-width
- `mx-auto` - Center horizontally
- `px-4` - Horizontal padding (mobile-friendly)
- `py-8` - Vertical padding
- `max-w-4xl` - Constrain content width for readability
- `space-y-6` - Vertical spacing between children
**When to use:**
- Blog posts
- Documentation pages
- Settings pages
- Any page with readable content
**[See live example](/dev/layouts#page-container)**
---
### 2. Dashboard Grid Pattern
**Use case**: Responsive card grid that adapts to screen size
```tsx
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map(item => (
<Card key={item.id}>
<CardHeader>
<CardTitle>{item.title}</CardTitle>
<CardDescription>{item.description}</CardDescription>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold">{item.value}</p>
</CardContent>
</Card>
))}
</div>
</div>
```
**Responsive behavior:**
- **Mobile** (`< 768px`): 1 column
- **Tablet** (`≥ 768px`): 2 columns
- **Desktop** (`≥ 1024px`): 3 columns
**Key Features:**
- `grid` - Use CSS Grid
- `grid-cols-1` - Default: 1 column (mobile-first)
- `md:grid-cols-2` - 2 columns on tablet
- `lg:grid-cols-3` - 3 columns on desktop
- `gap-6` - Consistent spacing between items
**When to use:**
- Dashboards
- Product grids
- Image galleries
- Card collections
**[See live example](/dev/layouts#dashboard-grid)**
---
### 3. Form Layout Pattern
**Use case**: Centered form with constrained width
```tsx
<div className="container mx-auto px-4 py-8">
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Login</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" placeholder="you@example.com" />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" type="password" />
</div>
<Button className="w-full">Sign In</Button>
</form>
</CardContent>
</Card>
</div>
```
**Key Features:**
- `max-w-md` - Constrain form width (448px max)
- `mx-auto` - Center the form
- `space-y-4` - Vertical spacing between fields
- `w-full` - Full-width button
**Form width guidelines:**
- **Short forms** (login, signup): `max-w-md` (448px)
- **Medium forms** (profile, settings): `max-w-lg` (512px)
- **Long forms** (checkout): `max-w-2xl` (672px)
**When to use:**
- Login/signup forms
- Contact forms
- Settings forms
- Any single-column form
**[See live example](/dev/layouts#form-layout)**
---
### 4. Sidebar Layout Pattern
**Use case**: Sidebar navigation with main content area
```tsx
<div className="flex min-h-screen">
{/* Sidebar */}
<aside className="w-64 border-r bg-muted/40 p-6">
<nav className="space-y-2">
<a href="#" className="block rounded-lg px-3 py-2 text-sm hover:bg-accent">
Dashboard
</a>
<a href="#" className="block rounded-lg px-3 py-2 text-sm hover:bg-accent">
Settings
</a>
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 p-6">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Page Title</h1>
{/* Content */}
</div>
</main>
</div>
```
**Key Features:**
- `flex` - Horizontal layout
- `w-64` - Fixed sidebar width (256px)
- `flex-1` - Main content takes remaining space
- `min-h-screen` - Full viewport height
- `border-r` - Visual separator
**Responsive strategy:**
```tsx
// Mobile: Collapsible sidebar
<div className="flex min-h-screen">
{/* Sidebar - hidden on mobile */}
<aside className="hidden lg:block w-64 border-r p-6">
{/* Sidebar content */}
</aside>
{/* Main content - full width on mobile */}
<main className="flex-1 p-4 lg:p-6">
{/* Content */}
</main>
</div>
// Add mobile menu button
<Button size="icon" className="lg:hidden">
<Menu className="h-6 w-6" />
</Button>
```
**When to use:**
- Admin dashboards
- Settings pages
- Documentation sites
- Apps with persistent navigation
**[See live example](/dev/layouts#sidebar-layout)**
---
### 5. Centered Content Pattern
**Use case**: Single-column content with optimal reading width
```tsx
<div className="container mx-auto px-4 py-8">
<article className="max-w-2xl mx-auto">
<h1 className="text-4xl font-bold mb-4">Article Title</h1>
<p className="text-muted-foreground mb-8">Published on Nov 2, 2025</p>
<div className="prose prose-lg">
<p>Article content with optimal line length for reading...</p>
<p>More content...</p>
</div>
</article>
</div>
```
**Key Features:**
- `max-w-2xl` - Optimal reading width (672px)
- `mx-auto` - Center content
- `prose` - Typography styles (if using @tailwindcss/typography)
**Width recommendations:**
- **Articles/Blogs**: `max-w-2xl` (672px)
- **Documentation**: `max-w-3xl` (768px)
- **Landing pages**: `max-w-4xl` (896px) or wider
- **Forms**: `max-w-md` (448px)
**When to use:**
- Blog posts
- Articles
- Documentation
- Long-form content
**[See live example](/dev/layouts#centered-content)**
---
## Responsive Strategies
### Mobile-First Approach
Always start with mobile layout, then enhance for larger screens:
```tsx
// ✅ CORRECT - Mobile first
<div className="
p-4 // Mobile: 16px padding
sm:p-6 // Tablet: 24px padding
lg:p-8 // Desktop: 32px padding
">
<div className="
grid
grid-cols-1 // Mobile: 1 column
sm:grid-cols-2 // Tablet: 2 columns
lg:grid-cols-3 // Desktop: 3 columns
gap-4
">
{/* Items */}
</div>
</div>
// ❌ WRONG - Desktop first
<div className="p-8 md:p-6 sm:p-4"> // Don't do this
```
### Breakpoints
| Breakpoint | Min Width | Typical Use |
|------------|-----------|-------------|
| `sm:` | 640px | Large phones, small tablets |
| `md:` | 768px | Tablets |
| `lg:` | 1024px | Laptops, desktops |
| `xl:` | 1280px | Large desktops |
| `2xl:` | 1536px | Extra large screens |
### Responsive Grid Columns
```tsx
// 1→2→3→4 progression (common)
grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4
// 1→2→3 progression (most common)
grid-cols-1 md:grid-cols-2 lg:grid-cols-3
// 1→2 progression (simple)
grid-cols-1 md:grid-cols-2
// 1→3 progression (skip 2)
grid-cols-1 lg:grid-cols-3
```
### Responsive Text
```tsx
// Heading sizes
<h1 className="
text-2xl sm:text-3xl lg:text-4xl
font-bold
">
Responsive Title
</h1>
// Body text (usually doesn't need responsive sizes)
<p className="text-base">
Body text stays consistent
</p>
```
---
## Common Mistakes
### ❌ Mistake 1: Using Margins Instead of Gap
```tsx
// ❌ WRONG - Children have margins
<div className="flex">
<div className="mr-4">Item 1</div>
<div className="mr-4">Item 2</div>
<div>Item 3</div> {/* Last one has no margin */}
</div>
// ✅ CORRECT - Parent controls spacing
<div className="flex gap-4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
```
### ❌ Mistake 2: Fixed Widths Instead of Responsive
```tsx
// ❌ WRONG - Fixed width, not responsive
<div className="w-[800px]">
Content
</div>
// ✅ CORRECT - Responsive width
<div className="w-full max-w-4xl mx-auto px-4">
Content
</div>
```
### ❌ Mistake 3: Not Using Container
```tsx
// ❌ WRONG - Content touches edges on large screens
<div className="px-4">
Content spans full width on 4K screens
</div>
// ✅ CORRECT - Container constrains width
<div className="container mx-auto px-4">
Content has maximum width
</div>
```
### ❌ Mistake 4: Desktop-First Responsive
```tsx
// ❌ WRONG - Desktop first
<div className="p-8 lg:p-6 md:p-4">
// ✅ CORRECT - Mobile first
<div className="p-4 md:p-6 lg:p-8">
```
### ❌ Mistake 5: Using Flex for Equal Columns
```tsx
// ❌ WRONG - Flex doesn't guarantee equal widths
<div className="flex gap-4">
<div className="flex-1">Col 1</div>
<div className="flex-1">Col 2</div>
<div className="flex-1">Col 3</div>
</div>
// ✅ CORRECT - Grid ensures equal widths
<div className="grid grid-cols-3 gap-4">
<div>Col 1</div>
<div>Col 2</div>
<div>Col 3</div>
</div>
```
**[See before/after examples](/dev/layouts)**
---
## Advanced Patterns
### Asymmetric Grid
```tsx
// 2/3 - 1/3 split
<div className="grid grid-cols-3 gap-6">
<div className="col-span-2">
Main content (2/3 width)
</div>
<div className="col-span-1">
Sidebar (1/3 width)
</div>
</div>
```
### Auto-fit Grid (Flexible columns)
```tsx
// Columns adjust based on available space
<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-6">
<Card>Item 1</Card>
<Card>Item 2</Card>
<Card>Item 3</Card>
{/* Adds as many columns as fit */}
</div>
```
### Sticky Sidebar
```tsx
<div className="flex gap-6">
<aside className="sticky top-6 h-fit w-64">
{/* Stays in view while scrolling */}
</aside>
<main className="flex-1">
{/* Scrollable content */}
</main>
</div>
```
### Full-height Layout
```tsx
<div className="flex flex-col min-h-screen">
<header className="h-16 border-b">Header</header>
<main className="flex-1">Flexible content</main>
<footer className="h-16 border-t">Footer</footer>
</div>
```
---
## Layout Checklist
Before implementing a layout, ask:
- [ ] **Responsive?** Does it work on mobile, tablet, desktop?
- [ ] **Container?** Is content constrained on large screens?
- [ ] **Spacing?** Using `gap` or `space-y`, not margins on children?
- [ ] **Mobile-first?** Starting with mobile layout?
- [ ] **Semantic?** Using appropriate HTML tags (main, aside, nav)?
- [ ] **Accessible?** Proper heading hierarchy, skip links?
---
## Quick Reference
### Grid Cheat Sheet
```tsx
// Basic grid
grid grid-cols-3 gap-6
// Responsive grid
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6
// Asymmetric grid
grid grid-cols-3 gap-6
<div className="col-span-2">...</div>
// Auto-fit grid
grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-6
```
### Flex Cheat Sheet
```tsx
// Horizontal flex
flex gap-4
// Vertical flex
flex flex-col gap-4
// Center items
flex items-center justify-center
// Space between
flex items-center justify-between
// Wrap items
flex flex-wrap gap-4
```
### Container Cheat Sheet
```tsx
// Standard container
container mx-auto px-4 py-8
// Constrained width
max-w-4xl mx-auto px-4
// Full width
w-full px-4
```
---
## Next Steps
- **Practice**: Build pages using the 5 essential patterns
- **Explore**: [Interactive layout examples](/dev/layouts)
- **Deep Dive**: [Spacing Philosophy](./04-spacing-philosophy.md)
- **Reference**: [Quick Reference Tables](./99-reference.md)
---
**Related Documentation:**
- [Spacing Philosophy](./04-spacing-philosophy.md) - When to use margin vs padding vs gap
- [Foundations](./01-foundations.md) - Spacing tokens and scale
- [Quick Start](./00-quick-start.md) - Essential patterns
**Last Updated**: November 2, 2025

View File

@@ -0,0 +1,708 @@
# Spacing Philosophy
**Master the "parent controls children" spacing strategy** that eliminates 90% of layout inconsistencies. Learn when to use margin, padding, or gap—and why children should never add their own margins.
---
## Table of Contents
1. [The Golden Rules](#the-golden-rules)
2. [Parent Controls Children Strategy](#parent-controls-children-strategy)
3. [Decision Tree: Margin vs Padding vs Gap](#decision-tree-margin-vs-padding-vs-gap)
4. [Common Patterns](#common-patterns)
5. [Before/After Examples](#beforeafter-examples)
6. [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
7. [Quick Reference](#quick-reference)
---
## The Golden Rules
These 5 rules eliminate 90% of spacing inconsistencies:
### Rule 1: Parent Controls Children
**Children don't add their own margins. The parent controls spacing between siblings.**
```tsx
// ✅ CORRECT - Parent controls spacing
<div className="space-y-4">
<Card>Item 1</Card>
<Card>Item 2</Card>
<Card>Item 3</Card>
</div>
// ❌ WRONG - Children add margins
<div>
<Card className="mb-4">Item 1</Card>
<Card className="mb-4">Item 2</Card>
<Card>Item 3</Card> {/* Inconsistent: last one has no margin */}
</div>
```
**Why this matters:**
- Eliminates "last child" edge cases
- Makes components reusable (they work in any context)
- Changes propagate from one place (parent)
- Prevents margin collapsing bugs
---
### Rule 2: Use Gap for Siblings
**For flex and grid layouts, use `gap-*` to space siblings.**
```tsx
// ✅ CORRECT - Gap for flex/grid
<div className="flex gap-4">
<Button>Cancel</Button>
<Button>Save</Button>
</div>
<div className="grid grid-cols-3 gap-6">
<Card>1</Card>
<Card>2</Card>
<Card>3</Card>
</div>
// ❌ WRONG - Children with margins
<div className="flex">
<Button className="mr-4">Cancel</Button>
<Button>Save</Button>
</div>
```
---
### Rule 3: Use Padding for Internal Spacing
**Padding is for spacing _inside_ a component, between the border and content.**
```tsx
// ✅ CORRECT - Padding for internal spacing
<Card className="p-6">
<CardTitle>Title</CardTitle>
<CardContent>Content</CardContent>
</Card>
// ❌ WRONG - Using margin for internal spacing
<Card>
<CardTitle className="m-6">Title</CardTitle>
</Card>
```
---
### Rule 4: Use space-y for Vertical Stacks
**For vertical stacks (not flex/grid), use `space-y-*` utility.**
```tsx
// ✅ CORRECT - space-y for stacks
<form className="space-y-4">
<Input />
<Input />
<Button />
</form>
// ❌ WRONG - Children with margins
<form>
<Input className="mb-4" />
<Input className="mb-4" />
<Button />
</form>
```
**How space-y works:**
```css
/* space-y-4 applies margin-top to all children except first */
.space-y-4 > * + * {
margin-top: 1rem; /* 16px */
}
```
---
### Rule 5: Margins Only for Exceptions
**Use margin only when a specific child needs different spacing from its siblings.**
```tsx
// ✅ CORRECT - Margin for exception
<div className="space-y-4">
<Card>Normal spacing</Card>
<Card>Normal spacing</Card>
<Card className="mt-8">Extra spacing above this one</Card>
<Card>Normal spacing</Card>
</div>
// Use case: Visually group related items
<div className="space-y-4">
<h2>Section 1</h2>
<p>Content</p>
<h2 className="mt-12">Section 2</h2> {/* Extra margin to separate sections */}
<p>Content</p>
</div>
```
---
## Parent Controls Children Strategy
### The Problem with Child-Controlled Spacing
When children control their own margins:
```tsx
// ❌ ANTI-PATTERN
function TodoItem({ className }: { className?: string }) {
return <div className={cn("mb-4", className)}>Todo</div>;
}
// Usage
<div>
<TodoItem /> {/* Has mb-4 */}
<TodoItem /> {/* Has mb-4 */}
<TodoItem /> {/* Has mb-4 - unwanted margin at bottom! */}
</div>
```
**Problems:**
1. ❌ Last item has unwanted margin
2. ❌ Can't change spacing without modifying component
3. ❌ Margin collapsing creates unpredictable spacing
4. ❌ Component not reusable in different contexts
---
### The Solution: Parent-Controlled Spacing
```tsx
// ✅ CORRECT PATTERN
function TodoItem({ className }: { className?: string }) {
return <div className={className}>Todo</div>; // No margin!
}
// Parent controls spacing
<div className="space-y-4">
<TodoItem />
<TodoItem />
<TodoItem /> {/* No unwanted margin */}
</div>
// Different context, different spacing
<div className="space-y-2">
<TodoItem />
<TodoItem />
</div>
// Another context, flex layout
<div className="flex gap-6">
<TodoItem />
<TodoItem />
</div>
```
**Benefits:**
1. ✅ No edge cases (last child, first child, only child)
2. ✅ Spacing controlled in one place
3. ✅ Component works in any layout context
4. ✅ No margin collapsing surprises
5. ✅ Easier to maintain and modify
---
## Decision Tree: Margin vs Padding vs Gap
Use this flowchart to choose the right spacing method:
```
┌─────────────────────────────────────────────┐
│ What are you spacing? │
└─────────────┬───────────────────────────────┘
┌──────┴──────┐
│ │
▼ ▼
Siblings? Inside a component?
│ │
│ └──> USE PADDING
│ className="p-4"
├──> Is parent using flex or grid?
│ │
│ ├─YES──> USE GAP
│ │ className="flex gap-4"
│ │ className="grid gap-6"
│ │
│ └─NO───> USE SPACE-Y or SPACE-X
│ className="space-y-4"
│ className="space-x-2"
└──> Exception case?
(One child needs different spacing)
└──> USE MARGIN
className="mt-8"
```
---
## Common Patterns
### Pattern 1: Form Fields (Vertical Stack)
```tsx
// ✅ CORRECT
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" />
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input id="password" />
</div>
<Button type="submit">Submit</Button>
</form>
```
**Spacing breakdown:**
- `space-y-4` on form: 16px between field groups
- `space-y-2` on field group: 8px between label and input
- No margins on children
---
### Pattern 2: Button Group (Horizontal Flex)
```tsx
// ✅ CORRECT
<div className="flex gap-4">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
// Responsive: stack on mobile, row on desktop
<div className="flex flex-col sm:flex-row gap-4">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
```
**Why gap over space-x:**
- Works with `flex-wrap`
- Works with `flex-col` (changes direction)
- Consistent spacing in all directions
---
### Pattern 3: Card Grid
```tsx
// ✅ CORRECT
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>Item 1</Card>
<Card>Item 2</Card>
<Card>Item 3</Card>
</div>
```
**Why gap:**
- Consistent spacing between rows and columns
- Works with responsive grid changes
- No edge cases (first row, last column, etc.)
---
### Pattern 4: Card Internal Spacing
```tsx
// ✅ CORRECT
<Card className="p-6">
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</CardContent>
<CardFooter className="pt-4">
<Button>Action</Button>
</CardFooter>
</Card>
```
**Spacing breakdown:**
- `p-6` on Card: 24px internal padding
- `space-y-4` on CardContent: 16px between paragraphs
- `pt-4` on CardFooter: Additional top padding for visual separation
---
### Pattern 5: Page Layout
```tsx
// ✅ CORRECT
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-3xl font-bold">Page Title</h1>
<Card>
<CardHeader>
<CardTitle>Section 1</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Section 2</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>
</div>
</div>
```
**Spacing breakdown:**
- `px-4`: Horizontal padding (prevents edge touching)
- `py-8`: Vertical padding (top and bottom spacing)
- `space-y-6`: 24px between sections
- No margins on children
---
## Before/After Examples
### Example 1: Button Group
#### ❌ Before (Child-Controlled)
```tsx
function ActionButton({ children, className }: Props) {
return <Button className={cn("mr-4", className)}>{children}</Button>;
}
// Usage
<div className="flex">
<ActionButton>Cancel</ActionButton>
<ActionButton>Save</ActionButton>
<ActionButton>Delete</ActionButton> {/* Unwanted mr-4 */}
</div>
```
**Problems:**
- Last button has unwanted margin
- Can't change spacing without modifying component
- Hard to use in vertical layout
#### ✅ After (Parent-Controlled)
```tsx
function ActionButton({ children, className }: Props) {
return <Button className={className}>{children}</Button>;
}
// Usage
<div className="flex gap-4">
<ActionButton>Cancel</ActionButton>
<ActionButton>Save</ActionButton>
<ActionButton>Delete</ActionButton>
</div>
// Different context: vertical
<div className="flex flex-col gap-2">
<ActionButton>Cancel</ActionButton>
<ActionButton>Save</ActionButton>
</div>
```
**Benefits:**
- No edge cases
- Reusable in any layout
- Easy to change spacing
---
### Example 2: List Items
#### ❌ Before (Child-Controlled)
```tsx
function ListItem({ title, description }: Props) {
return (
<div className="mb-6 p-4 border rounded">
<h3 className="mb-2">{title}</h3>
<p>{description}</p>
</div>
);
}
<div>
<ListItem title="Item 1" description="..." />
<ListItem title="Item 2" description="..." />
<ListItem title="Item 3" description="..." /> {/* Unwanted mb-6 */}
</div>
```
**Problems:**
- Last item has unwanted bottom margin
- Can't change list spacing without modifying component
- Internal `mb-2` hard to override
#### ✅ After (Parent-Controlled)
```tsx
function ListItem({ title, description }: Props) {
return (
<div className="p-4 border rounded">
<div className="space-y-2">
<h3 className="font-semibold">{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
<div className="space-y-6">
<ListItem title="Item 1" description="..." />
<ListItem title="Item 2" description="..." />
<ListItem title="Item 3" description="..." />
</div>
// Different context: compact spacing
<div className="space-y-2">
<ListItem title="Item 1" description="..." />
<ListItem title="Item 2" description="..." />
</div>
```
**Benefits:**
- No unwanted margins
- Internal spacing controlled by `space-y-2`
- Reusable with different spacings
---
### Example 3: Form Fields
#### ❌ Before (Mixed Strategy)
```tsx
<form>
<div className="mb-4">
<Label htmlFor="name">Name</Label>
<Input id="name" className="mt-2" />
</div>
<div className="mb-4">
<Label htmlFor="email">Email</Label>
<Input id="email" className="mt-2" />
</div>
<Button type="submit" className="mt-6">Submit</Button>
</form>
```
**Problems:**
- Spacing scattered across children
- Hard to change consistently
- Have to remember `mt-6` for button
#### ✅ After (Parent-Controlled)
```tsx
<form className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" />
</div>
<Button type="submit" className="mt-2">Submit</Button>
</form>
```
**Benefits:**
- Spacing controlled in 2 places: form (`space-y-4`) and field groups (`space-y-2`)
- Easy to change all field spacing at once
- Consistent and predictable
---
## Anti-Patterns to Avoid
### Anti-Pattern 1: Last Child Special Case
```tsx
// ❌ WRONG
{items.map((item, index) => (
<Card key={item.id} className={index < items.length - 1 ? "mb-4" : ""}>
{item.name}
</Card>
))}
// ✅ CORRECT
<div className="space-y-4">
{items.map(item => (
<Card key={item.id}>{item.name}</Card>
))}
</div>
```
---
### Anti-Pattern 2: Negative Margins to Fix Spacing
```tsx
// ❌ WRONG - Using negative margin to fix unwanted spacing
<div className="-mt-4"> {/* Canceling out previous margin */}
<Card>Content</Card>
</div>
// ✅ CORRECT - Parent controls spacing
<div className="space-y-4">
<Card>Content</Card>
</div>
```
**Why negative margins are bad:**
- Indicates broken spacing strategy
- Hard to maintain
- Creates coupling between components
---
### Anti-Pattern 3: Mixing Gap and Child Margins
```tsx
// ❌ WRONG - Gap + child margins = unpredictable spacing
<div className="flex gap-4">
<Button className="mr-2">Cancel</Button> {/* gap + mr-2 = 24px */}
<Button>Save</Button>
</div>
// ✅ CORRECT - Only gap
<div className="flex gap-4">
<Button>Cancel</Button>
<Button>Save</Button>
</div>
// ✅ CORRECT - Exception case
<div className="flex gap-4">
<Button>Cancel</Button>
<Button className="mr-8">Save</Button> {/* Intentional extra space */}
<Button>Delete</Button>
</div>
```
---
### Anti-Pattern 4: Using Margins for Layout
```tsx
// ❌ WRONG - Using margins to create layout
<div>
<div className="ml-64"> {/* Pushing content for sidebar */}
Content
</div>
</div>
// ✅ CORRECT - Use proper layout (flex/grid)
<div className="flex gap-6">
<aside className="w-64">Sidebar</aside>
<main className="flex-1">Content</main>
</div>
```
---
## Quick Reference
### Spacing Method Cheat Sheet
| Use Case | Method | Example |
|----------|--------|---------|
| **Flex siblings** | `gap-*` | `flex gap-4` |
| **Grid siblings** | `gap-*` | `grid gap-6` |
| **Vertical stack** | `space-y-*` | `space-y-4` |
| **Horizontal stack** | `space-x-*` | `space-x-2` |
| **Inside component** | `p-*` | `p-6` |
| **One child exception** | `m-*` | `mt-8` |
### Common Spacing Values
| Class | Pixels | Usage |
|-------|--------|-------|
| `gap-2` or `space-y-2` | 8px | Tight (label + input) |
| `gap-4` or `space-y-4` | 16px | Standard (form fields) |
| `gap-6` or `space-y-6` | 24px | Sections (cards) |
| `gap-8` or `space-y-8` | 32px | Large gaps |
| `p-4` | 16px | Standard padding |
| `p-6` | 24px | Card padding |
| `px-4 py-8` | 16px / 32px | Page padding |
### Decision Flowchart (Simplified)
```
Need spacing?
├─ Between siblings?
│ ├─ Flex/Grid parent? → gap-*
│ └─ Regular parent? → space-y-* or space-x-*
├─ Inside component? → p-*
└─ Exception case? → m-* (sparingly)
```
---
## Best Practices Summary
### Do ✅
1. **Use parent-controlled spacing** (`gap`, `space-y`, `space-x`)
2. **Use `gap-*` for flex and grid** layouts
3. **Use `space-y-*` for vertical stacks** (forms, content)
4. **Use `p-*` for internal spacing** (padding inside components)
5. **Use margin only for exceptions** (mt-8 to separate sections)
6. **Let components be context-agnostic** (no built-in margins)
### Don't ❌
1. ❌ Add margins to reusable components
2. ❌ Use last-child selectors or conditional margins
3. ❌ Mix gap with child margins
4. ❌ Use negative margins to fix spacing
5. ❌ Use margins for layout (use flex/grid)
6. ❌ Hard-code spacing in child components
---
## Spacing Checklist
Before implementing spacing, verify:
- [ ] **Parent controls children?** Using gap or space-y/x?
- [ ] **No child margins?** Components don't have mb-* or mr-*?
- [ ] **Consistent method?** Not mixing gap + child margins?
- [ ] **Reusable components?** Work in different contexts?
- [ ] **No edge cases?** No last-child or first-child special handling?
- [ ] **Semantic spacing?** Using design system scale (4, 8, 12, 16...)?
---
## Next Steps
- **Practice**: Refactor existing components to use parent-controlled spacing
- **Explore**: [Interactive spacing examples](/dev/spacing)
- **Reference**: [Quick Reference Tables](./99-reference.md)
- **Layout Patterns**: [Layouts Guide](./03-layouts.md)
---
**Related Documentation:**
- [Layouts](./03-layouts.md) - When to use Grid vs Flex
- [Foundations](./01-foundations.md) - Spacing scale tokens
- [Component Creation](./05-component-creation.md) - Building reusable components
- [Quick Start](./00-quick-start.md) - Essential patterns
**Last Updated**: November 2, 2025

View File

@@ -0,0 +1,874 @@
# 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

View File

@@ -0,0 +1,838 @@
# Forms Guide
**Master form patterns with react-hook-form + Zod validation**: Learn field layouts, error handling, loading states, and accessibility best practices for bulletproof forms.
---
## Table of Contents
1. [Form Architecture](#form-architecture)
2. [Basic Form Pattern](#basic-form-pattern)
3. [Field Patterns](#field-patterns)
4. [Validation with Zod](#validation-with-zod)
5. [Error Handling](#error-handling)
6. [Loading & Submit States](#loading--submit-states)
7. [Form Layouts](#form-layouts)
8. [Advanced Patterns](#advanced-patterns)
---
## Form Architecture
### Technology Stack
- **react-hook-form** - Form state management, validation
- **Zod** - Schema validation
- **@hookform/resolvers** - Zod resolver for react-hook-form
- **shadcn/ui components** - Input, Label, Button, etc.
**Why this stack?**
- ✅ Type-safe validation (TypeScript + Zod)
- ✅ Minimal re-renders (react-hook-form)
- ✅ Accessible by default (shadcn/ui)
- ✅ Easy error handling
- ✅ Built-in loading states
---
### Form Decision Tree
```
Need a form?
├─ Single field (search, filter)?
│ └─> Use uncontrolled input with onChange
│ <Input onChange={(e) => setQuery(e.target.value)} />
├─ Simple form (1-3 fields, no complex validation)?
│ └─> Use react-hook-form without Zod
│ const form = useForm();
└─ Complex form (4+ fields, validation, async submit)?
└─> Use react-hook-form + Zod
const form = useForm({ resolver: zodResolver(schema) });
```
---
## Basic Form Pattern
### Minimal Form (No Validation)
```tsx
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
interface FormData {
email: string;
}
export function SimpleForm() {
const form = useForm<FormData>();
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
{...form.register('email')}
/>
</div>
<Button type="submit">Submit</Button>
</form>
);
}
```
---
### Complete Form Pattern (with Zod)
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
// 1. Define validation schema
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
// 2. Infer TypeScript type from schema
type FormData = z.infer<typeof formSchema>;
export function LoginForm() {
// 3. Initialize form with Zod resolver
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
});
// 4. Submit handler (type-safe!)
const onSubmit = async (data: FormData) => {
try {
await loginUser(data);
toast.success('Logged in successfully');
} catch (error) {
toast.error('Invalid credentials');
}
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* Email field */}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
{...form.register('email')}
aria-invalid={!!form.formState.errors.email}
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
/>
{form.formState.errors.email && (
<p id="email-error" className="text-sm text-destructive">
{form.formState.errors.email.message}
</p>
)}
</div>
{/* Password field */}
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
{...form.register('password')}
aria-invalid={!!form.formState.errors.password}
aria-describedby={form.formState.errors.password ? 'password-error' : undefined}
/>
{form.formState.errors.password && (
<p id="password-error" className="text-sm text-destructive">
{form.formState.errors.password.message}
</p>
)}
</div>
{/* Submit button */}
<Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
{form.formState.isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</form>
);
}
```
**Key points:**
1. Define Zod schema first
2. Infer TypeScript type with `z.infer`
3. Use `zodResolver` in `useForm`
4. Register fields with `{...form.register('fieldName')}`
5. Show errors from `form.formState.errors`
6. Disable submit during submission
---
## Field Patterns
### Text Input
```tsx
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
{...form.register('name')}
aria-invalid={!!form.formState.errors.name}
aria-describedby={form.formState.errors.name ? 'name-error' : undefined}
/>
{form.formState.errors.name && (
<p id="name-error" className="text-sm text-destructive">
{form.formState.errors.name.message}
</p>
)}
</div>
```
---
### Textarea
```tsx
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
rows={4}
{...form.register('description')}
/>
{form.formState.errors.description && (
<p className="text-sm text-destructive">
{form.formState.errors.description.message}
</p>
)}
</div>
```
---
### Select
```tsx
<div className="space-y-2">
<Label htmlFor="role">Role</Label>
<Select
value={form.watch('role')}
onValueChange={(value) => form.setValue('role', value)}
>
<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>
{form.formState.errors.role && (
<p className="text-sm text-destructive">
{form.formState.errors.role.message}
</p>
)}
</div>
```
---
### Checkbox
```tsx
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={form.watch('acceptTerms')}
onCheckedChange={(checked) => form.setValue('acceptTerms', checked as boolean)}
/>
<Label htmlFor="terms" className="text-sm font-normal">
I accept the terms and conditions
</Label>
</div>
{form.formState.errors.acceptTerms && (
<p className="text-sm text-destructive">
{form.formState.errors.acceptTerms.message}
</p>
)}
```
---
### Radio Group (Custom Pattern)
```tsx
<div className="space-y-2">
<Label>Notification Method</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="radio"
id="email"
value="email"
{...form.register('notificationMethod')}
/>
<Label htmlFor="email" className="font-normal">Email</Label>
</div>
<div className="flex items-center space-x-2">
<input
type="radio"
id="sms"
value="sms"
{...form.register('notificationMethod')}
/>
<Label htmlFor="sms" className="font-normal">SMS</Label>
</div>
</div>
</div>
```
---
## Validation with Zod
### Common Validation Patterns
```tsx
import { z } from 'zod';
// Email
z.string().email('Invalid email address')
// Min/max length
z.string().min(8, 'Minimum 8 characters').max(100, 'Maximum 100 characters')
// Required field
z.string().min(1, 'This field is required')
// Optional field
z.string().optional()
// Number with range
z.number().min(0).max(100)
// Number from string input
z.coerce.number().min(0)
// Enum
z.enum(['admin', 'user', 'guest'], {
errorMap: () => ({ message: 'Invalid role' })
})
// URL
z.string().url('Invalid URL')
// Password with requirements
z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
// Confirm password
z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})
// Custom validation
z.string().refine((val) => !val.includes('badword'), {
message: 'Invalid input',
})
// Conditional fields
z.object({
role: z.enum(['admin', 'user']),
adminKey: z.string().optional(),
}).refine((data) => {
if (data.role === 'admin') {
return !!data.adminKey;
}
return true;
}, {
message: 'Admin key required for admin role',
path: ['adminKey'],
})
```
---
### Full Form Schema Example
```tsx
const userFormSchema = z.object({
// Required text
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
// Email
email: z.string().email('Invalid email address'),
// Optional phone
phone: z.string().optional(),
// Number
age: z.coerce.number().min(18, 'Must be 18 or older').max(120),
// Enum
role: z.enum(['admin', 'user', 'guest']),
// Boolean
acceptTerms: z.boolean().refine((val) => val === true, {
message: 'You must accept the terms',
}),
// Nested object
address: z.object({
street: z.string().min(1),
city: z.string().min(1),
zip: z.string().regex(/^\d{5}$/, 'Invalid ZIP code'),
}),
// Array
tags: z.array(z.string()).min(1, 'At least one tag required'),
});
type UserFormData = z.infer<typeof userFormSchema>;
```
---
## Error Handling
### Field-Level Errors
```tsx
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
{...form.register('email')}
className={form.formState.errors.email ? 'border-destructive' : ''}
aria-invalid={!!form.formState.errors.email}
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
/>
{form.formState.errors.email && (
<p id="email-error" className="text-sm text-destructive">
{form.formState.errors.email.message}
</p>
)}
</div>
```
**Accessibility notes:**
- Use `aria-invalid` to indicate error state
- Use `aria-describedby` to link error message
- Error ID format: `{fieldName}-error`
---
### Form-Level Errors
```tsx
const onSubmit = async (data: FormData) => {
try {
await submitForm(data);
} catch (error) {
// Set form-level error
form.setError('root', {
type: 'server',
message: error.message || 'Something went wrong',
});
}
};
// Display form-level error
{form.formState.errors.root && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
{form.formState.errors.root.message}
</AlertDescription>
</Alert>
)}
```
---
### Server Validation Errors
```tsx
const onSubmit = async (data: FormData) => {
try {
await createUser(data);
} catch (error) {
if (error.response?.data?.errors) {
// Map server errors to form fields
const serverErrors = error.response.data.errors;
Object.keys(serverErrors).forEach((field) => {
form.setError(field as keyof FormData, {
type: 'server',
message: serverErrors[field],
});
});
} else {
// Generic error
form.setError('root', {
type: 'server',
message: 'Failed to create user',
});
}
}
};
```
---
## Loading & Submit States
### Basic Loading State
```tsx
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
```
---
### Disable All Fields During Submit
```tsx
const isDisabled = form.formState.isSubmitting;
<Input
{...form.register('name')}
disabled={isDisabled}
/>
<Button type="submit" disabled={isDisabled}>
{isDisabled ? 'Submitting...' : 'Submit'}
</Button>
```
---
### Loading with Toast
```tsx
const onSubmit = async (data: FormData) => {
const loadingToast = toast.loading('Creating user...');
try {
await createUser(data);
toast.success('User created successfully', { id: loadingToast });
router.push('/users');
} catch (error) {
toast.error('Failed to create user', { id: loadingToast });
}
};
```
---
## Form Layouts
### Centered Form (Login, Signup)
```tsx
<div className="container mx-auto px-4 py-8">
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Sign In</CardTitle>
<CardDescription>Enter your credentials to continue</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* Form fields */}
<Button type="submit" className="w-full">
Sign In
</Button>
</form>
</CardContent>
</Card>
</div>
```
---
### Two-Column Form
```tsx
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Row 1: Two columns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="firstName">First Name</Label>
<Input id="firstName" {...form.register('firstName')} />
</div>
<div className="space-y-2">
<Label htmlFor="lastName">Last Name</Label>
<Input id="lastName" {...form.register('lastName')} />
</div>
</div>
{/* Row 2: Full width */}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" {...form.register('email')} />
</div>
<Button type="submit">Save</Button>
</form>
```
---
### Form with Sections
```tsx
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Section 1 */}
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">Personal Information</h3>
<p className="text-sm text-muted-foreground">
Basic details about you
</p>
</div>
<Separator />
<div className="space-y-4">
{/* Fields */}
</div>
</div>
{/* Section 2 */}
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">Account Settings</h3>
<p className="text-sm text-muted-foreground">
Configure your account preferences
</p>
</div>
<Separator />
<div className="space-y-4">
{/* Fields */}
</div>
</div>
<div className="flex justify-end gap-4">
<Button type="button" variant="outline">Cancel</Button>
<Button type="submit">Save Changes</Button>
</div>
</form>
```
---
## Advanced Patterns
### Dynamic Fields (Array)
```tsx
import { useFieldArray } from 'react-hook-form';
const schema = z.object({
items: z.array(z.object({
name: z.string().min(1),
quantity: z.coerce.number().min(1),
})).min(1, 'At least one item required'),
});
function DynamicForm() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
items: [{ name: '', quantity: 1 }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'items',
});
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex gap-4">
<Input
{...form.register(`items.${index}.name`)}
placeholder="Item name"
/>
<Input
type="number"
{...form.register(`items.${index}.quantity`)}
placeholder="Quantity"
/>
<Button
type="button"
variant="destructive"
onClick={() => remove(index)}
>
Remove
</Button>
</div>
))}
<Button
type="button"
variant="outline"
onClick={() => append({ name: '', quantity: 1 })}
>
Add Item
</Button>
<Button type="submit">Submit</Button>
</form>
);
}
```
---
### Conditional Fields
```tsx
const schema = z.object({
role: z.enum(['user', 'admin']),
adminKey: z.string().optional(),
}).refine((data) => {
if (data.role === 'admin') {
return !!data.adminKey;
}
return true;
}, {
message: 'Admin key required',
path: ['adminKey'],
});
function ConditionalForm() {
const form = useForm({ resolver: zodResolver(schema) });
const role = form.watch('role');
return (
<form className="space-y-4">
<Select
value={role}
onValueChange={(val) => form.setValue('role', val as any)}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
{role === 'admin' && (
<Input
{...form.register('adminKey')}
placeholder="Admin Key"
/>
)}
</form>
);
}
```
---
### File Upload
```tsx
const schema = z.object({
file: z.instanceof(FileList).refine((files) => files.length > 0, {
message: 'File is required',
}),
});
<input
type="file"
{...form.register('file')}
accept="image/*"
/>
const onSubmit = (data: FormData) => {
const file = data.file[0]; // FileList -> File
const formData = new FormData();
formData.append('file', file);
// Upload formData
};
```
---
## Form Checklist
Before shipping a form, verify:
### Functionality
- [ ] All fields register correctly
- [ ] Validation works (test invalid inputs)
- [ ] Submit handler fires
- [ ] Loading state works
- [ ] Error messages display
- [ ] Success case redirects/shows success
### Accessibility
- [ ] Labels associated with inputs (`htmlFor` + `id`)
- [ ] Error messages use `aria-describedby`
- [ ] Invalid inputs have `aria-invalid`
- [ ] Focus order is logical (Tab through form)
- [ ] Submit button disabled during submission
### UX
- [ ] Field errors appear on blur or submit
- [ ] Loading state prevents double-submit
- [ ] Success message or redirect on success
- [ ] Cancel button clears form or navigates away
- [ ] Mobile-friendly (responsive layout)
---
## Next Steps
- **Interactive Examples**: [Form examples](/dev/forms)
- **Components**: [Form components](./02-components.md#form-components)
- **Accessibility**: [Form accessibility](./07-accessibility.md#forms)
---
**Related Documentation:**
- [Components](./02-components.md) - Input, Label, Button, Select
- [Layouts](./03-layouts.md) - Form layout patterns
- [Accessibility](./07-accessibility.md) - ARIA attributes for forms
**External Resources:**
- [react-hook-form Documentation](https://react-hook-form.com)
- [Zod Documentation](https://zod.dev)
**Last Updated**: November 2, 2025

View File

@@ -0,0 +1,704 @@
# Accessibility Guide
**Build inclusive, accessible interfaces** that work for everyone. Learn WCAG AA standards, keyboard navigation, screen reader support, and testing strategies.
---
## Table of Contents
1. [Accessibility Standards](#accessibility-standards)
2. [Color Contrast](#color-contrast)
3. [Keyboard Navigation](#keyboard-navigation)
4. [Screen Reader Support](#screen-reader-support)
5. [ARIA Attributes](#aria-attributes)
6. [Focus Management](#focus-management)
7. [Testing](#testing)
8. [Accessibility Checklist](#accessibility-checklist)
---
## Accessibility Standards
### WCAG 2.1 Level AA
We follow **WCAG 2.1 Level AA** as the **minimum** standard.
**Why Level AA?**
- ✅ Required for most legal compliance (ADA, Section 508)
- ✅ Covers 95%+ of accessibility needs
- ✅ Achievable without major UX compromises
- ✅ Industry standard for modern web apps
**WCAG Principles (POUR):**
1. **Perceivable** - Information can be perceived by users
2. **Operable** - Interface can be operated by users
3. **Understandable** - Information and operation are understandable
4. **Robust** - Content works with current and future technologies
---
### Accessibility Decision Tree
```
Creating a UI element?
├─ Is it interactive?
│ ├─YES─> Can it be focused with Tab?
│ │ ├─YES─> ✅ Good
│ │ └─NO──> ❌ Add tabIndex or use button/link
│ │
│ └─NO──> Is it important information?
│ ├─YES─> Does it have appropriate semantic markup?
│ │ ├─YES─> ✅ Good
│ │ └─NO──> ❌ Use h1-h6, p, ul, etc.
│ │
│ └─NO──> Is it purely decorative?
│ ├─YES─> Add aria-hidden="true"
│ └─NO──> Add alt text or ARIA label
```
---
## Color Contrast
### Minimum Contrast Ratios (WCAG AA)
| Content Type | Minimum Ratio | Example |
|--------------|---------------|---------|
| **Normal text** (< 18px) | **4.5:1** | Body paragraphs, form labels |
| **Large text** (≥ 18px or ≥ 14px bold) | **3:1** | Headings, subheadings |
| **UI components** | **3:1** | Buttons, form borders, icons |
| **Graphical objects** | **3:1** | Chart elements, infographics |
**WCAG AAA (ideal, not required):**
- Normal text: 7:1
- Large text: 4.5:1
---
### Testing Color Contrast
**Tools:**
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- Chrome DevTools: Inspect element → Accessibility panel
- [Contrast Ratio Tool](https://contrast-ratio.com)
- Browser extensions: axe DevTools, WAVE
**Example:**
```tsx
// ✅ GOOD - 4.7:1 contrast (WCAG AA pass)
<p className="text-foreground"> // oklch(0.1529 0 0) on white
Body text
</p>
// ❌ BAD - 2.1:1 contrast (WCAG AA fail)
<p className="text-gray-400"> // Too light
Body text
</p>
// ✅ GOOD - Using semantic tokens ensures contrast
<p className="text-muted-foreground">
Secondary text
</p>
```
**Our design system tokens are WCAG AA compliant:**
- `text-foreground` on `bg-background`: 12.6:1 ✅
- `text-primary-foreground` on `bg-primary`: 8.2:1 ✅
- `text-destructive` on `bg-background`: 5.1:1 ✅
- `text-muted-foreground` on `bg-background`: 4.6:1 ✅
---
### Color Blindness
**8% of men and 0.5% of women** have some form of color blindness.
**Best practices:**
- ❌ Don't rely on color alone to convey information
- ✅ Use icons, text labels, or patterns in addition to color
- ✅ Test with color blindness simulators
**Example:**
```tsx
// ❌ BAD - Color only
<div className="text-green-600">Success</div>
<div className="text-red-600">Error</div>
// ✅ GOOD - Color + icon + text
<Alert variant="success">
<CheckCircle className="h-4 w-4" />
<AlertTitle>Success</AlertTitle>
<AlertDescription>Operation completed</AlertDescription>
</Alert>
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>Something went wrong</AlertDescription>
</Alert>
```
---
## Keyboard Navigation
### Core Requirements
All interactive elements must be:
1.**Focusable** - Can be reached with Tab key
2.**Activatable** - Can be triggered with Enter or Space
3.**Navigable** - Can move between with arrow keys (where appropriate)
4.**Escapable** - Can be closed/exited with Escape key
---
### Tab Order
**Natural tab order** follows DOM order (top to bottom, left to right).
```tsx
// ✅ GOOD - Natural tab order
<form>
<Input /> {/* Tab 1 */}
<Input /> {/* Tab 2 */}
<Button>Submit</Button> {/* Tab 3 */}
</form>
// ❌ BAD - Using tabIndex to force order
<form>
<Input tabIndex={2} /> // Don't do this
<Input tabIndex={1} />
<Button tabIndex={3}>Submit</Button>
</form>
```
**When to use `tabIndex`:**
- `tabIndex={0}` - Make non-interactive element focusable
- `tabIndex={-1}` - Remove from tab order (for programmatic focus)
- `tabIndex={1+}` - ❌ **Avoid** - Breaks natural order
---
### Keyboard Shortcuts
| Key | Action | Example |
|-----|--------|---------|
| **Tab** | Move focus forward | Navigate through form fields |
| **Shift + Tab** | Move focus backward | Go back to previous field |
| **Enter** | Activate button/link | Submit form, follow link |
| **Space** | Activate button/checkbox | Toggle checkbox, click button |
| **Escape** | Close overlay | Close dialog, dropdown |
| **Arrow keys** | Navigate within component | Navigate dropdown items |
| **Home** | Jump to start | First item in list |
| **End** | Jump to end | Last item in list |
---
### Implementing Keyboard Navigation
**Button (automatic):**
```tsx
// ✅ Button is keyboard accessible by default
<Button onClick={handleClick}>
Click me
</Button>
// Enter or Space triggers onClick
```
**Custom clickable div (needs work):**
```tsx
// ❌ BAD - Not keyboard accessible
<div onClick={handleClick}>
Click me
</div>
// ✅ GOOD - Make it accessible
<div
role="button"
tabIndex={0}
onClick={handleClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}}
>
Click me
</div>
// ✅ BETTER - Just use a button
<button onClick={handleClick}>
Click me
</button>
```
**Dropdown navigation:**
```tsx
<DropdownMenu>
<DropdownMenuContent>
<DropdownMenuItem>Edit</DropdownMenuItem> {/* Arrow down */}
<DropdownMenuItem>Delete</DropdownMenuItem> {/* Arrow down */}
</DropdownMenuContent>
</DropdownMenu>
// shadcn/ui handles arrow key navigation automatically
```
---
### Skip Links
**Allow keyboard users to skip navigation:**
```tsx
// Add to layout
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary focus:text-primary-foreground focus:rounded-lg"
>
Skip to main content
</a>
<nav>{/* Navigation */}</nav>
<main id="main-content">
{/* Main content */}
</main>
```
---
## Screen Reader Support
### Screen Reader Basics
**Popular screen readers:**
- **NVDA** (Windows) - Free, most popular for testing
- **JAWS** (Windows) - Industry standard, paid
- **VoiceOver** (macOS/iOS) - Built-in to Apple devices
- **TalkBack** (Android) - Built-in to Android
**What screen readers announce:**
- Semantic element type (button, link, heading, etc.)
- Element text content
- Element state (expanded, selected, disabled)
- ARIA labels and descriptions
---
### Semantic HTML
**Use the right HTML element for the job:**
```tsx
// ✅ GOOD - Semantic HTML
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>&copy; 2025 Company</p>
</footer>
// ❌ BAD - Div soup
<div>
<div>
<div>
<div onClick={goHome}>Home</div>
<div onClick={goAbout}>About</div>
</div>
</div>
</div>
<div>
<div>
<div>Page Title</div>
<div>Content...</div>
</div>
</div>
```
**Semantic elements:**
- `<header>` - Page header
- `<nav>` - Navigation
- `<main>` - Main content (only one per page)
- `<article>` - Self-contained content
- `<section>` - Thematic grouping
- `<aside>` - Sidebar content
- `<footer>` - Page footer
- `<h1>` - `<h6>` - Headings (hierarchical)
- `<button>` - Buttons
- `<a>` - Links
---
### Alt Text for Images
```tsx
// ✅ GOOD - Descriptive alt text
<img src="/chart.png" alt="Bar chart showing 20% increase in sales from January to February" />
// ✅ GOOD - Decorative images
<img src="/decorative.png" alt="" /> // Empty alt for decorative
// OR
<img src="/decorative.png" aria-hidden="true" />
// ❌ BAD - Generic or missing alt
<img src="/chart.png" alt="image" />
<img src="/chart.png" /> // No alt
```
**Icon-only buttons:**
```tsx
// ✅ GOOD - ARIA label
<Button size="icon" aria-label="Close dialog">
<X className="h-4 w-4" />
</Button>
// ❌ BAD - No label
<Button size="icon">
<X className="h-4 w-4" />
</Button>
```
---
## ARIA Attributes
### Common ARIA Attributes
**ARIA roles:**
```tsx
<div role="button" tabIndex={0}>Custom Button</div>
<div role="alert">Error message</div>
<div role="status">Loading...</div>
<div role="navigation">...</div>
```
**ARIA states:**
```tsx
<button aria-expanded={isOpen}>Toggle Menu</button>
<button aria-pressed={isActive}>Toggle</button>
<input aria-invalid={!!errors.email} />
<div aria-disabled="true">Disabled Item</div>
```
**ARIA properties:**
```tsx
<button aria-label="Close">×</button>
<input aria-describedby="email-help" />
<input aria-required="true" />
<div aria-live="polite">Status updates</div>
<div aria-hidden="true">Decorative content</div>
```
---
### Form Accessibility
**Label association:**
```tsx
// ✅ GOOD - Explicit association
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
// ❌ BAD - No association
<div>Email</div>
<Input type="email" />
```
**Error messages:**
```tsx
// ✅ GOOD - Linked with aria-describedby
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
aria-invalid={!!errors.password}
aria-describedby={errors.password ? 'password-error' : undefined}
/>
{errors.password && (
<p id="password-error" className="text-sm text-destructive">
{errors.password.message}
</p>
)}
// ❌ BAD - No association
<Input type="password" />
{errors.password && <p>{errors.password.message}</p>}
```
**Required fields:**
```tsx
// ✅ GOOD - Marked as required
<Label htmlFor="name">
Name <span className="text-destructive">*</span>
</Label>
<Input id="name" required aria-required="true" />
// Screen reader announces: "Name, required, edit text"
```
---
### Live Regions
**Announce dynamic updates:**
```tsx
// Polite (waits for user to finish)
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
// Assertive (interrupts immediately)
<div aria-live="assertive" role="alert">
{errorMessage}
</div>
// Example: Toast notifications (sonner uses this)
toast.success('User created');
// Announces: "Success. User created."
```
---
## Focus Management
### Visible Focus Indicators
**All interactive elements must have visible focus:**
```tsx
// ✅ GOOD - shadcn/ui components have focus rings
<Button>Click me</Button>
// Shows ring on focus
// ✅ GOOD - Custom focus styles
<div
tabIndex={0}
className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Focusable content
</div>
// ❌ BAD - Removing focus outline
<button style={{ outline: 'none' }}>Bad</button>
```
**Use `:focus-visible` instead of `:focus`:**
- `:focus` - Shows on mouse click AND keyboard
- `:focus-visible` - Shows only on keyboard (better UX)
---
### Focus Trapping
**Dialogs should trap focus:**
```tsx
// shadcn/ui Dialog automatically traps focus
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent>
{/* Focus trapped inside */}
<Input autoFocus /> {/* Focus first field */}
<Button>Submit</Button>
</DialogContent>
</Dialog>
// When dialog closes, focus returns to trigger button
```
---
### Programmatic Focus
**Set focus after actions:**
```tsx
const inputRef = useRef<HTMLInputElement>(null);
const handleDelete = () => {
deleteUser();
// Return focus to a relevant element
inputRef.current?.focus();
};
<Input ref={inputRef} />
```
---
## Testing
### Automated Testing Tools
**Browser extensions:**
- [axe DevTools](https://www.deque.com/axe/devtools/) - Free, comprehensive
- [WAVE](https://wave.webaim.org/extension/) - Visual feedback
- [Lighthouse](https://developer.chrome.com/docs/lighthouse/) - Built into Chrome
**CI/CD testing:**
- [@axe-core/react](https://github.com/dequelabs/axe-core-npm/tree/develop/packages/react) - Runtime accessibility testing
- [jest-axe](https://github.com/nickcolley/jest-axe) - Jest integration
- [Playwright accessibility testing](https://playwright.dev/docs/accessibility-testing)
---
### Manual Testing Checklist
#### Keyboard Testing
1. [ ] Unplug mouse
2. [ ] Tab through entire page
3. [ ] All interactive elements focusable?
4. [ ] Focus indicators visible?
5. [ ] Can activate with Enter/Space?
6. [ ] Can close modals with Escape?
7. [ ] Tab order logical?
#### Screen Reader Testing
1. [ ] Install NVDA (Windows) or VoiceOver (Mac)
2. [ ] Navigate page with screen reader on
3. [ ] All content announced?
4. [ ] Interactive elements have labels?
5. [ ] Form errors announced?
6. [ ] Heading hierarchy correct?
#### Contrast Testing
1. [ ] Use contrast checker on all text
2. [ ] Check UI components (buttons, borders)
3. [ ] Test in dark mode too
4. [ ] All elements meet 4.5:1 (text) or 3:1 (UI)?
---
### Testing with Real Users
**Considerations:**
- Test with actual users who rely on assistive technologies
- Different screen readers behave differently
- Mobile screen readers (VoiceOver, TalkBack) differ from desktop
- Keyboard-only users have different needs than screen reader users
---
## Accessibility Checklist
### General
- [ ] Page has `<title>` and `<meta name="description">`
- [ ] Page has proper heading hierarchy (h1 → h2 → h3)
- [ ] Landmarks used (`<header>`, `<nav>`, `<main>`, `<footer>`)
- [ ] Skip link present for keyboard users
- [ ] No content relies on color alone
### Color & Contrast
- [ ] Text has 4.5:1 contrast (normal) or 3:1 (large)
- [ ] UI components have 3:1 contrast
- [ ] Tested in both light and dark modes
- [ ] Color blindness simulator used
### Keyboard
- [ ] All interactive elements focusable
- [ ] Focus indicators visible (ring, outline, etc.)
- [ ] Tab order is logical
- [ ] No keyboard traps
- [ ] Enter/Space activates buttons
- [ ] Escape closes dialogs/dropdowns
- [ ] Arrow keys navigate lists/menus
### Screen Readers
- [ ] All images have alt text
- [ ] Icon-only buttons have aria-label
- [ ] Form labels associated with inputs
- [ ] Form errors use aria-describedby
- [ ] Required fields marked with aria-required
- [ ] Live regions for dynamic updates
- [ ] ARIA roles used correctly
### Forms
- [ ] Labels associated with inputs (`htmlFor` + `id`)
- [ ] Error messages linked (`aria-describedby`)
- [ ] Invalid inputs marked (`aria-invalid`)
- [ ] Required fields indicated (`aria-required`)
- [ ] Submit button disabled during submission
### Focus Management
- [ ] Dialogs trap focus
- [ ] Focus returns after dialog closes
- [ ] Programmatic focus after actions
- [ ] No focus outline removed without alternative
---
## Quick Wins for Accessibility
**Easy improvements with big impact:**
1. **Add alt text to images**
```tsx
<img src="/logo.png" alt="Company Logo" />
```
2. **Associate labels with inputs**
```tsx
<Label htmlFor="email">Email</Label>
<Input id="email" />
```
3. **Use semantic HTML**
```tsx
<button> instead of <div onClick>
```
4. **Add aria-label to icon buttons**
```tsx
<Button aria-label="Close"><X /></Button>
```
5. **Use semantic color tokens**
```tsx
className="text-foreground" // Auto contrast
```
6. **Test with keyboard only**
- Tab through page
- Fix anything unreachable
---
## Next Steps
- **Test Now**: Run [axe DevTools](https://www.deque.com/axe/devtools/) on your app
- **Learn More**: [W3C ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
- **Components**: [Review accessible components](./02-components.md)
- **Forms**: [Accessible form patterns](./06-forms.md)
---
**Related Documentation:**
- [Forms](./06-forms.md) - Accessible form patterns
- [Components](./02-components.md) - All components are accessible
- [Foundations](./01-foundations.md) - Color contrast tokens
**External Resources:**
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- [A11y Project Checklist](https://www.a11yproject.com/checklist/)
- [axe DevTools](https://www.deque.com/axe/devtools/)
**Last Updated**: November 2, 2025

View File

@@ -0,0 +1,574 @@
# AI Code Generation Guidelines
**For AI Assistants**: This document contains strict rules for generating code in the FastNext Template project. Follow these rules to ensure generated code matches the design system perfectly.
---
## 🎯 Core Rules
### ALWAYS Do
1.**Import from `@/components/ui/*`**
```tsx
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
```
2. ✅ **Use semantic color tokens**
```tsx
className="bg-primary text-primary-foreground"
className="text-destructive"
className="bg-muted text-muted-foreground"
```
3. ✅ **Use `cn()` utility for className merging**
```tsx
import { cn } from '@/lib/utils';
className={cn("base-classes", conditional && "conditional-classes", className)}
```
4. ✅ **Follow spacing scale** (multiples of 4: 0, 1, 2, 3, 4, 6, 8, 12, 16)
```tsx
className="p-4 space-y-6 mb-8"
```
5. ✅ **Add accessibility attributes**
```tsx
<Label htmlFor="email">Email</Label>
<Input
id="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
```
6. ✅ **Use component variants**
```tsx
<Button variant="destructive">Delete</Button>
<Alert variant="destructive">Error message</Alert>
```
7. ✅ **Compose from shadcn/ui primitives**
```tsx
// Don't create custom card components
// Use Card + CardHeader + CardTitle + CardContent
```
8. ✅ **Use mobile-first responsive design**
```tsx
className="text-2xl sm:text-3xl lg:text-4xl"
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
```
---
### NEVER Do
1. ❌ **NO arbitrary colors**
```tsx
// ❌ WRONG
className="bg-blue-500 text-white"
// ✅ CORRECT
className="bg-primary text-primary-foreground"
```
2. ❌ **NO arbitrary spacing values**
```tsx
// ❌ WRONG
className="p-[13px] mb-[17px]"
// ✅ CORRECT
className="p-4 mb-4"
```
3. ❌ **NO inline styles**
```tsx
// ❌ WRONG
style={{ margin: '10px', color: '#3b82f6' }}
// ✅ CORRECT
className="m-4 text-primary"
```
4. ❌ **NO custom CSS classes** (use Tailwind utilities)
```tsx
// ❌ WRONG
<div className="my-custom-class">
// ✅ CORRECT
<div className="flex items-center justify-between p-4">
```
5. ❌ **NO mixing component libraries**
```tsx
// ❌ WRONG - Don't use Material-UI, Ant Design, etc.
import { Button } from '@mui/material';
// ✅ CORRECT - Only shadcn/ui
import { Button } from '@/components/ui/button';
```
6. ❌ **NO skipping accessibility**
```tsx
// ❌ WRONG
<button><X /></button>
// ✅ CORRECT
<Button size="icon" aria-label="Close">
<X className="h-4 w-4" />
</Button>
```
7. ❌ **NO creating custom variants without CVA**
```tsx
// ❌ WRONG
<Button className={type === 'danger' ? 'bg-red-500' : 'bg-blue-500'}>
// ✅ CORRECT
<Button variant="destructive">Delete</Button>
```
---
## 📐 Layout Patterns
### Page Container
```tsx
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Content */}
</div>
</div>
```
### Dashboard Grid
```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map(item => <Card key={item.id}>...</Card>)}
</div>
```
### Form Layout
```tsx
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Form Title</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
{/* Form fields */}
</form>
</CardContent>
</Card>
```
### Centered Content
```tsx
<div className="max-w-2xl mx-auto px-4">
{/* Readable content width */}
</div>
```
---
## 🧩 Component Templates
### Custom Component Template
```tsx
import { cn } from '@/lib/utils';
import { Card } from '@/components/ui/card';
interface MyComponentProps {
variant?: 'default' | 'compact';
className?: string;
children: React.ReactNode;
}
export function MyComponent({
variant = 'default',
className,
children
}: MyComponentProps) {
return (
<Card className={cn(
"p-4", // base styles
variant === 'compact' && "p-2",
className // allow overrides
)}>
{children}
</Card>
);
}
```
### Component with CVA (class-variance-authority)
```tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const componentVariants = cva(
"base-classes-here", // base
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-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 ComponentProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof componentVariants> {}
export function Component({ variant, size, className, ...props }: ComponentProps) {
return (
<div className={cn(componentVariants({ variant, size, className }))} {...props} />
);
}
```
---
## 📝 Form Pattern Template
```tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert } from '@/components/ui/alert';
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type FormData = z.infer<typeof formSchema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
email: '',
password: '',
},
});
const onSubmit = async (data: FormData) => {
// Handle submission
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* Email Field */}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
{...form.register('email')}
aria-invalid={!!form.formState.errors.email}
aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
/>
{form.formState.errors.email && (
<p id="email-error" className="text-sm text-destructive">
{form.formState.errors.email.message}
</p>
)}
</div>
{/* Submit Button */}
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</form>
);
}
```
---
## 🎨 Color Token Reference
**Always use these semantic tokens:**
| Token | Usage |
|-------|-------|
| `bg-primary text-primary-foreground` | Primary buttons, CTAs |
| `bg-secondary text-secondary-foreground` | Secondary actions |
| `bg-destructive text-destructive-foreground` | Delete, errors |
| `bg-muted text-muted-foreground` | Disabled states |
| `bg-accent text-accent-foreground` | Hover states |
| `bg-card text-card-foreground` | Card backgrounds |
| `text-foreground` | Body text |
| `text-muted-foreground` | Secondary text |
| `border-border` | Borders |
| `ring-ring` | Focus rings |
---
## 📏 Spacing Reference
**Use these spacing values (multiples of 4px):**
| Class | Value | Pixels | Usage |
|-------|-------|--------|-------|
| `2` | 0.5rem | 8px | Tight spacing |
| `4` | 1rem | 16px | Standard spacing |
| `6` | 1.5rem | 24px | Section spacing |
| `8` | 2rem | 32px | Large gaps |
| `12` | 3rem | 48px | Section dividers |
| `16` | 4rem | 64px | Page sections |
---
## 🔑 Decision Trees
### When to use Grid vs Flex?
```
Need equal-width columns? → Use Grid
className="grid grid-cols-3 gap-6"
Need flexible item sizes? → Use Flex
className="flex gap-4"
Need 2D layout (rows + columns)? → Use Grid
className="grid grid-cols-2 grid-rows-3 gap-4"
Need 1D layout (single row OR column)? → Use Flex
className="flex flex-col gap-4"
```
### When to use Margin vs Padding?
```
Spacing between sibling elements? → Use gap or space-y
className="flex gap-4"
className="space-y-4"
Internal element spacing? → Use padding
className="p-4"
External element spacing? → Avoid margins, use parent gap
// ❌ Child with margin
<div className="mb-4">
// ✅ Parent with gap
<div className="space-y-4">
```
---
## 🚨 Common Mistakes to Avoid
### ❌ Mistake 1: Hardcoding colors
```tsx
// ❌ WRONG
<div className="bg-red-500 text-white">Error</div>
// ✅ CORRECT
<Alert variant="destructive">Error message</Alert>
```
### ❌ Mistake 2: Arbitrary spacing
```tsx
// ❌ WRONG
<div className="p-[15px] mb-[23px]">
// ✅ CORRECT
<div className="p-4 mb-6">
```
### ❌ Mistake 3: Missing accessibility
```tsx
// ❌ WRONG
<input type="email" />
// ✅ CORRECT
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
```
### ❌ Mistake 4: Creating custom components unnecessarily
```tsx
// ❌ WRONG - Custom component for simple composition
function MyCard({ title, children }) {
return <div className="card">{children}</div>;
}
// ✅ CORRECT - Use shadcn/ui primitives
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
```
### ❌ Mistake 5: Not using cn() utility
```tsx
// ❌ WRONG
<div className={`base-class ${isActive ? 'active-class' : ''} ${className}`}>
// ✅ CORRECT
<div className={cn("base-class", isActive && "active-class", className)}>
```
---
## 📚 Reference Documentation
Before generating code, check these resources:
1. **[Quick Start](./00-quick-start.md)** - Essential patterns
2. **[Components](./02-components.md)** - All shadcn/ui components
3. **[Layouts](./03-layouts.md)** - Layout patterns
4. **[Spacing](./04-spacing-philosophy.md)** - Spacing rules
5. **[Forms](./06-forms.md)** - Form patterns
6. **[Reference](./99-reference.md)** - Quick lookup tables
---
## ✅ Code Generation Checklist
Before outputting code, verify:
- [ ] All imports from `@/components/ui/*`
- [ ] Using semantic color tokens (no `bg-blue-500`)
- [ ] Using spacing scale (multiples of 4)
- [ ] Using `cn()` for className merging
- [ ] Accessibility attributes included
- [ ] Mobile-first responsive design
- [ ] Composing from shadcn/ui primitives
- [ ] Following established patterns from docs
- [ ] No inline styles
- [ ] No arbitrary values
---
## 🤖 AI Assistant Configuration
### For Claude Code / Cursor
Add this to your project context:
```
When generating React/Next.js components:
1. Always import from @/components/ui/*
2. Use semantic tokens (bg-primary, text-destructive)
3. Use cn() utility for classNames
4. Follow spacing scale (4, 8, 12, 16, 24, 32)
5. Add accessibility (labels, ARIA)
6. Use component variants (variant="destructive")
7. Reference: /docs/design-system/08-ai-guidelines.md
```
### For GitHub Copilot
Add to `.github/copilot-instructions.md`:
```markdown
# Component Guidelines
- Import from @/components/ui/*
- Use semantic colors: bg-primary, text-destructive
- Spacing: multiples of 4 (p-4, mb-6, gap-8)
- Use cn() for className merging
- Add accessibility attributes
- See /docs/design-system/08-ai-guidelines.md
```
---
## 📊 Examples
### ✅ Good Component (AI Generated)
```tsx
import { cn } from '@/lib/utils';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
interface DashboardCardProps {
title: string;
value: string;
trend?: 'up' | 'down';
className?: string;
}
export function DashboardCard({ title, value, trend, className }: DashboardCardProps) {
return (
<Card className={cn("p-6", className)}>
<CardHeader>
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{trend && (
<p className={cn(
"text-xs",
trend === 'up' && "text-green-600",
trend === 'down' && "text-destructive"
)}>
{trend === 'up' ? '↑' : '↓'} Trend
</p>
)}
</CardContent>
</Card>
);
}
```
**Why it's good:**
- ✅ Imports from `@/components/ui/*`
- ✅ Uses semantic tokens
- ✅ Uses `cn()` utility
- ✅ Follows spacing scale
- ✅ Composes from shadcn/ui primitives
- ✅ TypeScript interfaces
- ✅ Allows className override
---
## 🎓 Learning Path for AI
1. Read [Quick Start](./00-quick-start.md) - Essential patterns
2. Read this document - Rules and templates
3. Reference [Component Guide](./02-components.md) - All components
4. Check [Reference Tables](./99-reference.md) - Token lookups
With these guidelines, you can generate code that perfectly matches the design system. Always prioritize consistency over creativity.
---
**Last Updated**: November 2, 2025
**For AI Assistants**: Follow these rules strictly for optimal code generation.

View File

@@ -0,0 +1,599 @@
# Quick Reference
**Bookmark this page** for instant lookups of colors, spacing, typography, components, and common patterns. Your go-to cheat sheet for the FastNext design system.
---
## Table of Contents
1. [Color Tokens](#color-tokens)
2. [Typography Scale](#typography-scale)
3. [Spacing Scale](#spacing-scale)
4. [Component Variants](#component-variants)
5. [Layout Patterns](#layout-patterns)
6. [Common Class Combinations](#common-class-combinations)
7. [Decision Trees](#decision-trees)
---
## Color Tokens
### Semantic Colors
| Token | Usage | Example |
|-------|-------|---------|
| `bg-primary text-primary-foreground` | CTAs, primary actions | Primary button |
| `bg-secondary text-secondary-foreground` | Secondary actions | Secondary button |
| `bg-destructive text-destructive-foreground` | Delete, errors | Delete button, error alert |
| `bg-muted text-muted-foreground` | Disabled, subtle | Disabled button, TabsList |
| `bg-accent text-accent-foreground` | Hover states | Dropdown hover |
| `bg-card text-card-foreground` | Cards, elevated surfaces | Card component |
| `bg-popover text-popover-foreground` | Popovers, dropdowns | Dropdown content |
| `bg-background text-foreground` | Page background | Body |
| `text-foreground` | Body text | Paragraphs |
| `text-muted-foreground` | Secondary text | Captions, helper text |
| `border-border` | Borders, dividers | Card borders, separators |
| `border-input` | Input borders | Text input border |
| `ring-ring` | Focus indicators | Focus ring |
### Usage Examples
```tsx
// Primary button
<Button className="bg-primary text-primary-foreground">Save</Button>
// Destructive button
<Button className="bg-destructive text-destructive-foreground">Delete</Button>
// Secondary text
<p className="text-muted-foreground text-sm">Helper text</p>
// Card
<Card className="bg-card text-card-foreground border-border">...</Card>
// Focus ring
<div className="focus-visible:ring-2 focus-visible:ring-ring">...</div>
```
---
## Typography Scale
### Font Sizes
| Class | rem | px | Use Case | Common |
|-------|-----|----|----|:------:|
| `text-xs` | 0.75rem | 12px | Labels, fine print | |
| `text-sm` | 0.875rem | 14px | Secondary text, captions | ⭐ |
| `text-base` | 1rem | 16px | Body text (default) | ⭐ |
| `text-lg` | 1.125rem | 18px | Subheadings | |
| `text-xl` | 1.25rem | 20px | Card titles | ⭐ |
| `text-2xl` | 1.5rem | 24px | Section headings | ⭐ |
| `text-3xl` | 1.875rem | 30px | Page titles | ⭐ |
| `text-4xl` | 2.25rem | 36px | Large headings | |
| `text-5xl` | 3rem | 48px | Hero text | |
⭐ = Most commonly used
### Font Weights
| Class | Value | Use Case | Common |
|-------|-------|----------|:------:|
| `font-light` | 300 | De-emphasized text | |
| `font-normal` | 400 | Body text (default) | ⭐ |
| `font-medium` | 500 | Labels, menu items | ⭐ |
| `font-semibold` | 600 | Subheadings, buttons | ⭐ |
| `font-bold` | 700 | Headings, emphasis | ⭐ |
⭐ = Most commonly used
### Common Typography Combinations
```tsx
// Page title
<h1 className="text-3xl font-bold">Page Title</h1>
// Section heading
<h2 className="text-2xl font-semibold mb-4">Section Heading</h2>
// Card title
<h3 className="text-xl font-semibold">Card Title</h3>
// Body text (default)
<p className="text-base text-foreground">Regular paragraph</p>
// Secondary text
<p className="text-sm text-muted-foreground">Helper text</p>
// Label
<Label className="text-sm font-medium">Field Label</Label>
```
---
## Spacing Scale
### Spacing Values
| Token | rem | px | Use Case | Common |
|-------|-----|----|----|:------:|
| `0` | 0 | 0px | No spacing | |
| `px` | - | 1px | Borders | |
| `0.5` | 0.125rem | 2px | Very tight | |
| `1` | 0.25rem | 4px | Icon gaps | |
| `2` | 0.5rem | 8px | Tight spacing (label → input) | ⭐ |
| `3` | 0.75rem | 12px | Component padding | |
| `4` | 1rem | 16px | Standard spacing (form fields) | ⭐ |
| `5` | 1.25rem | 20px | Medium spacing | |
| `6` | 1.5rem | 24px | Section spacing (cards) | ⭐ |
| `8` | 2rem | 32px | Large gaps | ⭐ |
| `10` | 2.5rem | 40px | Very large gaps | |
| `12` | 3rem | 48px | Section dividers | ⭐ |
| `16` | 4rem | 64px | Page sections | |
⭐ = Most commonly used
### Spacing Methods
| Method | Use Case | Example |
|--------|----------|---------|
| `gap-4` | Flex/grid spacing | `flex gap-4` |
| `space-y-4` | Vertical stack spacing | `space-y-4` |
| `space-x-4` | Horizontal stack spacing | `space-x-4` |
| `p-4` | Padding (all sides) | `p-4` |
| `px-4` | Horizontal padding | `px-4` |
| `py-4` | Vertical padding | `py-4` |
| `m-4` | Margin (exceptions only!) | `mt-8` |
### Common Spacing Patterns
```tsx
// Form vertical spacing
<form className="space-y-4">...</form>
// Field group spacing (label → input)
<div className="space-y-2">
<Label>...</Label>
<Input />
</div>
// Button group horizontal spacing
<div className="flex gap-4">
<Button>Cancel</Button>
<Button>Save</Button>
</div>
// Card grid spacing
<div className="grid grid-cols-3 gap-6">...</div>
// Page padding
<div className="container mx-auto px-4 py-8">...</div>
// Card padding
<Card className="p-6">...</Card>
```
---
## Component Variants
### Button Variants
```tsx
<Button variant="default">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
<Button variant="destructive">Delete</Button>
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
```
### Badge Variants
```tsx
<Badge variant="default">New</Badge>
<Badge variant="secondary">Draft</Badge>
<Badge variant="outline">Pending</Badge>
<Badge variant="destructive">Critical</Badge>
```
### Alert Variants
```tsx
<Alert variant="default">Info alert</Alert>
<Alert variant="destructive">Error alert</Alert>
```
---
## Layout Patterns
### Grid Columns
```tsx
// 1 → 2 → 3 progression (most common)
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
// 1 → 2 → 4 progression
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"
// 1 → 2 progression (simple)
className="grid grid-cols-1 md:grid-cols-2 gap-6"
// 1 → 3 progression (skip 2)
className="grid grid-cols-1 lg:grid-cols-3 gap-6"
```
### Container Widths
```tsx
// Standard container
className="container mx-auto px-4"
// Constrained widths
className="max-w-md mx-auto" // 448px - Forms
className="max-w-lg mx-auto" // 512px - Modals
className="max-w-2xl mx-auto" // 672px - Articles
className="max-w-4xl mx-auto" // 896px - Wide layouts
className="max-w-7xl mx-auto" // 1280px - Full page
```
### Flex Patterns
```tsx
// Horizontal flex
className="flex gap-4"
// Vertical flex
className="flex flex-col gap-4"
// Center items
className="flex items-center justify-center"
// Space between
className="flex items-center justify-between"
// Wrap items
className="flex flex-wrap gap-4"
// Responsive: stack on mobile, row on desktop
className="flex flex-col sm:flex-row gap-4"
```
---
## Common Class Combinations
### Page Container
```tsx
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Content */}
</div>
</div>
```
### Card Header with Action
```tsx
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</div>
<Button variant="outline" size="sm">Action</Button>
</CardHeader>
```
### Dashboard Metric Card Header
```tsx
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Metric Title</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
```
### Form Field
```tsx
<div className="space-y-2">
<Label htmlFor="field">Label</Label>
<Input id="field" />
{error && <p className="text-sm text-destructive">{error.message}</p>}
</div>
```
### Centered Form Card
```tsx
<div className="container mx-auto px-4 py-8">
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Form Title</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4">
{/* Fields */}
</form>
</CardContent>
</Card>
</div>
```
### Button Group
```tsx
<div className="flex gap-4">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
// Or right-aligned
<div className="flex justify-end gap-4">
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</div>
```
### Icon with Text
```tsx
<div className="flex items-center gap-2">
<Icon className="h-4 w-4" />
<span>Text</span>
</div>
```
### Responsive Text
```tsx
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">
Responsive Title
</h1>
```
### Responsive Padding
```tsx
<div className="p-4 sm:p-6 lg:p-8">
Responsive padding
</div>
```
---
## Decision Trees
### Grid vs Flex
```
Need equal-width columns? → Grid
Example: grid grid-cols-3 gap-6
Need flexible item sizes? → Flex
Example: flex gap-4
Need 2D layout (rows + columns)? → Grid
Example: grid grid-cols-2 grid-rows-3 gap-4
Need 1D layout (row OR column)? → Flex
Example: flex flex-col gap-4
```
### Margin vs Padding vs Gap
```
Spacing between siblings?
├─ Flex/Grid parent? → gap
└─ Regular parent? → space-y or space-x
Inside component? → padding
Exception case (one child different)? → margin
```
### Button Variant
```
What's the action?
├─ Primary action (save, submit) → variant="default"
├─ Secondary action (cancel, back) → variant="secondary"
├─ Alternative action (view, edit) → variant="outline"
├─ Subtle action (icon in list) → variant="ghost"
├─ In-text action (learn more) → variant="link"
└─ Delete/remove action → variant="destructive"
```
### Form Field Error Display
```
Has error?
├─YES─> Add aria-invalid={true}
│ Add aria-describedby="field-error"
│ Add border-destructive class
│ Show <p id="field-error" className="text-sm text-destructive">
└─NO──> Normal state
```
---
## Keyboard Shortcuts
| Key | Action | Context |
|-----|--------|---------|
| `Tab` | Move focus forward | All |
| `Shift + Tab` | Move focus backward | All |
| `Enter` | Activate button/link | Buttons, links |
| `Space` | Activate button/checkbox | Buttons, checkboxes |
| `Escape` | Close overlay | Dialogs, dropdowns |
| `Arrow keys` | Navigate items | Dropdowns, lists |
| `Home` | Jump to start | Lists |
| `End` | Jump to end | Lists |
---
## Accessibility Quick Checks
### Contrast Ratios
- **Normal text (< 18px)**: 4.5:1 minimum
- **Large text (≥ 18px or ≥ 14px bold)**: 3:1 minimum
- **UI components**: 3:1 minimum
### ARIA Attributes
```tsx
// Icon-only button
<Button size="icon" aria-label="Close">
<X className="h-4 w-4" />
</Button>
// Form field error
<Input
aria-invalid={!!error}
aria-describedby={error ? 'field-error' : undefined}
/>
{error && <p id="field-error">{error.message}</p>}
// Required field
<Input aria-required="true" required />
// Live region
<div aria-live="polite">{statusMessage}</div>
```
---
## Import Cheat Sheet
```tsx
// Components
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
// Utilities
import { cn } from '@/lib/utils';
// Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Toast
import { toast } from 'sonner';
// Icons
import { Check, X, AlertCircle, Loader2 } from 'lucide-react';
```
---
## Zod Validation Patterns
```tsx
// Required string
z.string().min(1, 'Required')
// Email
z.string().email('Invalid email')
// Min/max length
z.string().min(8, 'Min 8 chars').max(100, 'Max 100 chars')
// Optional
z.string().optional()
// Number
z.coerce.number().min(0).max(100)
// Enum
z.enum(['admin', 'user', 'guest'])
// Boolean
z.boolean().refine(val => val === true, { message: 'Must accept' })
// Password confirmation
z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
})
```
---
## Responsive Breakpoints
| Breakpoint | Min Width | Typical Device |
|------------|-----------|----------------|
| `sm:` | 640px | Large phones, small tablets |
| `md:` | 768px | Tablets |
| `lg:` | 1024px | Laptops, desktops |
| `xl:` | 1280px | Large desktops |
| `2xl:` | 1536px | Extra large screens |
```tsx
// Mobile-first (default → sm → md → lg)
className="text-sm sm:text-base md:text-lg lg:text-xl"
className="p-4 sm:p-6 lg:p-8"
className="grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
```
---
## Shadows & Radius
### Shadows
```tsx
shadow-sm // Cards, panels
shadow-md // Dropdowns, tooltips
shadow-lg // Modals, popovers
shadow-xl // Floating notifications
```
### Border Radius
```tsx
rounded-sm // 2px - Tags, small badges
rounded-md // 4px - Inputs, small buttons
rounded-lg // 6px - Cards, buttons (default)
rounded-xl // 10px - Large cards, modals
rounded-full // Pills, avatars, icon buttons
```
---
## Next Steps
- **For detailed info**: Navigate to specific guides from [README](./README.md)
- **For examples**: Visit [/dev/components](/dev/components)
- **For AI**: See [AI Guidelines](./08-ai-guidelines.md)
---
**Related Documentation:**
- [Quick Start](./00-quick-start.md) - 5-minute crash course
- [Foundations](./01-foundations.md) - Detailed color, typography, spacing
- [Components](./02-components.md) - All component variants
- [Layouts](./03-layouts.md) - Layout patterns
- [Forms](./06-forms.md) - Form patterns
- [Accessibility](./07-accessibility.md) - WCAG compliance
**Last Updated**: November 2, 2025

View File

@@ -0,0 +1,304 @@
# Design System Documentation
**FastNext Template Design System** - A comprehensive guide to building consistent, accessible, and beautiful user interfaces.
---
## 🚀 Quick Navigation
| For... | Start Here | Time |
|--------|-----------|------|
| **Quick Start** | [⚡ 5-Minute Crash Course](./00-quick-start.md) | 5 min |
| **Component Development** | [🧩 Components](./02-components.md) → [🔨 Creation Guide](./05-component-creation.md) | 15 min |
| **Layout Design** | [📐 Layouts](./03-layouts.md) → [📏 Spacing](./04-spacing-philosophy.md) | 20 min |
| **AI Code Generation** | [🤖 AI Guidelines](./08-ai-guidelines.md) | 3 min |
| **Quick Reference** | [📚 Reference Tables](./99-reference.md) | Instant |
| **Complete Guide** | Read all docs in order | 1 hour |
---
## 📖 Documentation Structure
### Getting Started
- **[00. Quick Start](./00-quick-start.md)** ⚡
- 5-minute crash course
- Essential components and patterns
- Copy-paste ready examples
### Fundamentals
- **[01. Foundations](./01-foundations.md)** 🎨
- Color system (OKLCH)
- Typography scale
- Spacing tokens
- Shadows & radius
- **[02. Components](./02-components.md)** 🧩
- shadcn/ui component library
- All variants documented
- Usage examples
- Composition patterns
### Layouts & Spacing
- **[03. Layouts](./03-layouts.md)** 📐
- Grid vs Flex decision tree
- Common layout patterns
- Responsive strategies
- Before/after examples
- **[04. Spacing Philosophy](./04-spacing-philosophy.md)** 📏
- Parent vs child spacing rules
- Margin vs padding strategy
- Gap vs margin for flex/grid
- Consistency patterns
### Building Components
- **[05. Component Creation](./05-component-creation.md)** 🔨
- When to create vs compose
- Component templates
- Variant patterns (CVA)
- Testing checklist
- **[06. Forms](./06-forms.md)** 📝
- Form patterns & validation
- Error state UI
- Loading states
- Multi-field examples
### Best Practices
- **[07. Accessibility](./07-accessibility.md)** ♿
- WCAG AA compliance
- Keyboard navigation
- Screen reader support
- ARIA attributes
- **[08. AI Guidelines](./08-ai-guidelines.md)** 🤖
- Rules for AI code generation
- Required patterns
- Forbidden practices
- Component templates
### Reference
- **[99. Reference Tables](./99-reference.md)** 📚
- Quick lookup tables
- All tokens at a glance
- Cheat sheet
---
## 🎪 Interactive Examples
Explore live examples and copy-paste code:
- **[Component Showcase](/dev/components)** - All shadcn/ui components with variants
- **[Layout Patterns](/dev/layouts)** - Before/after comparisons of layouts
- **[Spacing Examples](/dev/spacing)** - Visual spacing demonstrations
- **[Form Patterns](/dev/forms)** - Complete form examples
Each demo page includes:
- ✅ Live, interactive examples
- ✅ Click-to-copy code snippets
- ✅ Before/after comparisons
- ✅ Links to documentation
---
## 🛤️ Learning Paths
### Path 1: Speedrun (5 minutes)
**Goal**: Start building immediately
1. [Quick Start](./00-quick-start.md) - Essential patterns
2. [Reference](./99-reference.md) - Bookmark for lookup
3. Start coding!
**When to use**: You need to build something NOW and will learn deeply later.
---
### Path 2: Component Developer (15 minutes)
**Goal**: Master component building
1. [Quick Start](./00-quick-start.md) - Basics
2. [Components](./02-components.md) - shadcn/ui library
3. [Component Creation](./05-component-creation.md) - Building custom components
4. [Reference](./99-reference.md) - Bookmark
**When to use**: You're building reusable components or UI library.
---
### Path 3: Layout Specialist (20 minutes)
**Goal**: Master layouts and spacing
1. [Quick Start](./00-quick-start.md) - Basics
2. [Foundations](./01-foundations.md) - Spacing tokens
3. [Layouts](./03-layouts.md) - Grid vs Flex patterns
4. [Spacing Philosophy](./04-spacing-philosophy.md) - Margin/padding rules
5. [Reference](./99-reference.md) - Bookmark
**When to use**: You're designing page layouts or dashboard UIs.
---
### Path 4: Form Specialist (15 minutes)
**Goal**: Master forms and validation
1. [Quick Start](./00-quick-start.md) - Basics
2. [Components](./02-components.md) - Form components
3. [Forms](./06-forms.md) - Patterns & validation
4. [Accessibility](./07-accessibility.md) - ARIA for forms
5. [Reference](./99-reference.md) - Bookmark
**When to use**: You're building forms with complex validation.
---
### Path 5: AI Setup (3 minutes)
**Goal**: Configure AI for perfect code generation
1. [AI Guidelines](./08-ai-guidelines.md) - Read once, code forever
2. Reference this in your AI context/prompts
**When to use**: You're using AI assistants (Claude, GitHub Copilot, etc.) to generate code.
---
### Path 6: Comprehensive Mastery (1 hour)
**Goal**: Complete understanding of the design system
Read all documents in order:
1. [Quick Start](./00-quick-start.md)
2. [Foundations](./01-foundations.md)
3. [Components](./02-components.md)
4. [Layouts](./03-layouts.md)
5. [Spacing Philosophy](./04-spacing-philosophy.md)
6. [Component Creation](./05-component-creation.md)
7. [Forms](./06-forms.md)
8. [Accessibility](./07-accessibility.md)
9. [AI Guidelines](./08-ai-guidelines.md)
10. [Reference](./99-reference.md)
Explore all [interactive demos](/dev).
**When to use**: You're the design system maintainer or want complete mastery.
---
## 🎯 Key Principles
Our design system is built on these core principles:
1. **🎨 Semantic First** - Use `bg-primary`, not `bg-blue-500`
2. **♿ Accessible by Default** - WCAG AA minimum, keyboard-first
3. **📐 Consistent Spacing** - Multiples of 4px (0.25rem)
4. **🧩 Compose, Don't Create** - Use shadcn/ui primitives
5. **🌗 Dark Mode Ready** - All components work in light/dark
6. **⚡ Pareto Efficient** - 80% of needs with 20% of patterns
---
## 🏗️ Technology Stack
- **Framework**: Next.js 15 + React 19
- **Styling**: Tailwind CSS 4 (CSS-first configuration)
- **Components**: shadcn/ui (New York style)
- **Color Space**: OKLCH (perceptually uniform)
- **Icons**: lucide-react
- **Fonts**: Geist Sans + Geist Mono
---
## 🤝 Contributing to the Design System
### Adding a New Component
1. Read [Component Creation Guide](./05-component-creation.md)
2. Follow the template
3. Add to [Component Showcase](/dev/components)
4. Document in [Components](./02-components.md)
### Adding a New Pattern
1. Validate it solves a real need (used 3+ times)
2. Document in appropriate guide
3. Add to [Reference](./99-reference.md)
4. Create example in `/dev/`
### Updating Colors/Tokens
1. Edit `src/app/globals.css`
2. Test in both light and dark modes
3. Verify WCAG AA contrast
4. Update [Foundations](./01-foundations.md)
---
## 📝 Quick Reference
### Most Common Patterns
```tsx
// Button
<Button variant="default">Action</Button>
<Button variant="destructive">Delete</Button>
// Card
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>
// Form Input
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" {...field} />
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
)}
// Layout
<div className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* Content */}
</div>
</div>
// Grid
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map(item => <Card key={item.id}>...</Card>)}
</div>
```
---
## 🆘 Need Help?
1. **Quick Answer**: Check [Reference](./99-reference.md)
2. **Pattern Question**: Search relevant doc (Layouts, Components, etc.)
3. **Can't Find It**: Browse [Interactive Examples](/dev)
4. **Still Stuck**: Read [Quick Start](./00-quick-start.md) or [Comprehensive Guide](#path-6-comprehensive-mastery-1-hour)
---
## 📊 Design System Metrics
- **Components**: 20+ shadcn/ui components
- **Color Tokens**: 25+ semantic color variables
- **Layout Patterns**: 5 essential patterns (80% coverage)
- **Spacing Scale**: 14 token sizes (0-16)
- **Typography Scale**: 9 sizes (xs-9xl)
- **Test Coverage**: All patterns demonstrated in /dev/
---
## 📚 External Resources
- [shadcn/ui Documentation](https://ui.shadcn.com)
- [Tailwind CSS 4 Documentation](https://tailwindcss.com/docs)
- [OKLCH Color Picker](https://oklch.com)
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [Radix UI Primitives](https://www.radix-ui.com/primitives)
---
**Last Updated**: November 2, 2025
**Version**: 1.0
**Maintainer**: Design System Team