Episode 1 — Fundamentals / 1.9 — CSS Responsive Design

1.9.f — Spacing & Rhythm

In one sentence: A consistent spacing system built on a base unit (4px or 8px) eliminates visual randomness, speeds up development decisions, and makes responsive adjustments predictable.

Navigation: ← 1.9.e — Fluid Typography · 1.9.g — CSS Frameworks Overview →


1. What is vertical rhythm?

Vertical rhythm means the vertical spacing between elements follows a consistent pattern — like lines on ruled paper. When rhythm is consistent, the page feels balanced and professional. When it's random, the design looks thrown together.

Good rhythm (consistent)        Poor rhythm (random)
┌──────────────────┐            ┌──────────────────┐
│  Heading         │ 24px       │  Heading         │ 24px
│                  │            │                  │
│  Paragraph       │ 16px       │  Paragraph       │ 13px
│                  │            │                  │
│  Paragraph       │ 16px       │                  │
│                  │            │  Paragraph       │ 22px
│  Subheading      │ 24px       │                  │
│                  │            │  Subheading      │ 8px
│  Paragraph       │ 16px       │  Paragraph       │ 16px
└──────────────────┘            └──────────────────┘

2. Choosing a base unit

Most design systems use 4px or 8px as the base unit. Every spacing value is a multiple of that base.

4px base (more granular)

4  →  0.25rem
8  →  0.5rem
12 →  0.75rem
16 →  1rem
20 →  1.25rem
24 →  1.5rem
32 →  2rem
40 →  2.5rem
48 →  3rem
64 →  4rem

8px base (simpler, fewer choices)

8  →  0.5rem
16 →  1rem
24 →  1.5rem
32 →  2rem
48 →  3rem
64 →  4rem
96 →  6rem

Why multiples work: Consistent increments align elements on a visual grid, making the design feel structured. Sub-pixel rounding issues are minimized with clean multiples.


3. Spacing scale tokens

Encode your scale as CSS custom properties so every developer uses the same values:

:root {
  --space-1:  0.25rem;  /*  4px */
  --space-2:  0.5rem;   /*  8px */
  --space-3:  0.75rem;  /* 12px */
  --space-4:  1rem;     /* 16px */
  --space-5:  1.25rem;  /* 20px */
  --space-6:  1.5rem;   /* 24px */
  --space-8:  2rem;     /* 32px */
  --space-10: 2.5rem;   /* 40px */
  --space-12: 3rem;     /* 48px */
  --space-16: 4rem;     /* 64px */
  --space-20: 5rem;     /* 80px */
  --space-24: 6rem;     /* 96px */
}

Usage:

.card {
  padding: var(--space-4);
  margin-bottom: var(--space-6);
}

.section {
  padding-block: var(--space-12);
}

.stack > * + * {
  margin-top: var(--space-4);
}

Now if you need to audit or adjust spacing globally, you change the tokens — not hundreds of individual values.


4. Horizontal spacing: margins and padding

Horizontal spacing follows the same scale but serves different purposes:

PropertyUse caseExample
padding-inlineInternal breathing roomCard content away from edges
margin-inline: autoCentering block elementsContainer centering
gapSpace between flex/grid childrenGrid gutter
column-gapHorizontal grid/flex gapMulti-column layouts
.container {
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--space-4);
}

@media (min-width: 768px) {
  .container {
    padding-inline: var(--space-8);
  }
}

5. Using gap for consistent component spacing

Before gap, developers used margins on children and :first-child / :last-child hacks to remove extra space. gap eliminates this entirely:

/* Old way — fragile */
.list-item {
  margin-bottom: 1rem;
}
.list-item:last-child {
  margin-bottom: 0;
}

/* Modern way — clean */
.list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

gap works in Flexbox, Grid, and multi-column layouts:

/* Flex row with gap */
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* Grid with different row/column gaps */
.dashboard {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--space-6);
  row-gap: var(--space-8);
}

6. The "stack" and "cluster" patterns

Stack — vertical spacing between siblings

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

Every direct child is spaced identically. No margin management needed.

Cluster — horizontal wrapping with consistent gaps

.cluster {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

Tags, badges, or inline actions wrap naturally with uniform spacing.

Stack (vertical)              Cluster (horizontal wrap)
┌──────────────┐              ┌──────────────────────┐
│  Item A      │              │ [Tag] [Tag] [Tag]    │
│   ↕ gap      │              │  ↔gap  ↔gap          │
│  Item B      │              │ [Tag] [Tag]          │
│   ↕ gap      │              └──────────────────────┘
│  Item C      │
└──────────────┘

7. Responsive spacing that scales

Use clamp() for spacing that grows with the viewport — same technique as fluid typography:

:root {
  --space-section: clamp(2rem, 5vw, 6rem);
  --space-card:    clamp(1rem, 2vw, 2rem);
  --space-gutter:  clamp(1rem, 3vw, 3rem);
}

.hero {
  padding-block: var(--space-section);
}

.card {
  padding: var(--space-card);
}

.grid {
  gap: var(--space-gutter);
}

Or change tokens at breakpoints:

:root {
  --space-section: 2rem;
}

@media (min-width: 768px) {
  :root { --space-section: 4rem; }
}

@media (min-width: 1200px) {
  :root { --space-section: 6rem; }
}

Both approaches work. clamp() is smoother; breakpoint-based is more explicit.


8. Avoiding magic numbers

A magic number is a hard-coded value with no clear origin:

/* Bad — where did these come from? */
.header { padding: 13px 27px; margin-bottom: 43px; }

Problems:

  • No relationship to other spacing on the page.
  • Impossible to adjust globally.
  • Creates visual inconsistency.

Fix: Use your spacing tokens:

/* Good — from the system */
.header {
  padding: var(--space-3) var(--space-6);
  margin-bottom: var(--space-10);
}

When a token doesn't fit: If you need 13px, you probably need 12px (--space-3) or 16px (--space-4). Round to the nearest token. If you truly need a one-off value, comment why:

.logo {
  /* Optical alignment: the logo's internal padding makes 12px look off-center */
  margin-top: 14px;
}

9. Putting it all together

:root {
  --space-2:  0.5rem;
  --space-4:  1rem;
  --space-6:  1.5rem;
  --space-8:  2rem;
  --space-12: 3rem;
  --space-16: 4rem;
}

body {
  font-size: clamp(1rem, 0.34vw + 0.91rem, 1.125rem);
  line-height: 1.6;
}

.page {
  max-width: 1200px;
  margin-inline: auto;
  padding-inline: var(--space-4);
}

.section {
  padding-block: clamp(var(--space-8), 5vw, var(--space-16));
}

.section > * + * {
  margin-top: var(--space-4);
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-6);
}

.card {
  padding: var(--space-6);
  border-radius: var(--space-2);
}

10. Key takeaways

  1. Pick a base unit (4px or 8px) and build every spacing value from multiples of it.
  2. Encode the scale as CSS custom properties (tokens) — never hard-code random values.
  3. Use gap on flex/grid instead of margin hacks on children.
  4. Stack and cluster patterns cover most vertical and horizontal spacing needs.
  5. Make spacing responsive with clamp() or breakpoint-based token updates.
  6. Eliminate magic numbers — round to the nearest token and document exceptions.

Explain-It Challenge

Explain without notes:

  1. Why does a 4px/8px spacing system make design reviews and code reviews faster?
  2. Your colleague uses margin-bottom on every child plus :last-child { margin-bottom: 0 }. What's a cleaner modern approach?
  3. How would you make section padding scale smoothly from 2rem on mobile to 6rem on desktop without breakpoints?

Navigation: ← 1.9.e — Fluid Typography · 1.9.g — CSS Frameworks Overview →