Files
fast-next-template/frontend/docs/design-system/04-spacing-philosophy.md
Felipe Cardoso 96df7edf88 Refactor useAuth hook, settings components, and docs for formatting and readability improvements
- Consolidated multi-line arguments into single lines where appropriate in `useAuth`.
- Improved spacing and readability in data processing across components (`ProfileSettingsForm`, `PasswordChangeForm`, `SessionCard`).
- Applied consistent table and markdown formatting in design system docs (e.g., `README.md`, `08-ai-guidelines.md`, `00-quick-start.md`).
- Updated code snippets to ensure adherence to Prettier rules and streamlined JSX structures.
2025-11-10 11:03:45 +01:00

17 KiB

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
  2. Parent Controls Children Strategy
  3. Decision Tree: Margin vs Padding vs Gap
  4. Common Patterns
  5. Before/After Examples
  6. Anti-Patterns to Avoid
  7. 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.

// ✅ 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.

// ✅ 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.

// ✅ 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.

// ✅ 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:

/* 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.

// ✅ 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:

// ❌ 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

// ✅ 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)

// ✅ 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)

// ✅ 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

// ✅ 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

// ✅ 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

// ✅ 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)

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)

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)

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)

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)

<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)

<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

// ❌ 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

// ❌ 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

// ❌ 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

// ❌ 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


Related Documentation:

Last Updated: November 2, 2025