Web Layout & Spacing
The definitive reference for layout and spacing in modern web design. Not "use flexbox" — the actual systems, principles, and math behind why good layouts feel right and bad ones feel off.
Spacing is the invisible skeleton of every interface. Color gets the glory, typography gets the respect, but spacing is what makes both of them work. A page with perfect type and color but broken spacing looks amateur. A page with mediocre type but perfect spacing looks professional. That is the power of getting this right.
When to Use
Use for:
- Building any web layout from scratch
- Establishing spacing tokens for a design system
- Debugging layouts that "look wrong" but you cannot articulate why
- Converting Figma designs to CSS with accurate spacing
- Making responsive layouts that feel right on every screen size
- Setting up vertical rhythm for long-form content
- Auditing existing sites for spacing inconsistencies
- Card layouts, form layouts, dashboard grids, content pages
Do NOT use for:
- Animation and motion design (distances yes, timing/easing no) --> use motion-design-web
- Typography decisions (font choice, pairing) --> use typography-expert
- Full design system architecture --> use design-system-bootstrap
- Color and contrast decisions --> use the appropriate color/accessibility skill
- Page-level information architecture --> use web-design-expert
The 8px Grid System
The 8px grid is the industry standard for spacing. Apple, Google, and most major design systems use it. The reasons are mathematical: 8 divides cleanly into common screen resolutions (320, 360, 375, 390, 414, 768, 1024, 1280, 1440, 1920), it scales cleanly at 0.75x (6px), 1.5x (12px), and 2x (16px) for device pixel ratios, and it produces spacing values large enough to be visually distinct but small enough to be granular.
The Base Scale
Step Value Tailwind v4 Use Case
─────────────────────────────────────────────────────────
0 0px 0 Reset/collapse
0.5 2px 0.5 Hairline borders, fine adjustments
1 4px 1 Icon padding, tight inline spacing
2 8px 2 Compact element gaps, small padding
3 12px 3 Form field inner padding, list gaps
4 16px 4 Standard component padding, paragraph spacing
5 20px 5 Card inner padding (compact)
6 24px 6 Section sub-gaps, card padding (standard)
8 32px 8 Between related sections, form groups
10 40px 10 Between distinct sections
12 48px 12 Major section spacing
16 64px 16 Page section dividers
20 80px 20 Hero-to-content transition
24 96px 24 Maximum page section spacing
CSS Custom Properties — The Foundation
Every project should define its spacing scale once. Everything else references it.
:root {
/* Base unit: 8px (0.5rem at default 16px root) */
--space-unit: 0.5rem;
/* The scale */
--space-0: 0;
--space-0-5: calc(0.25 * var(--space-unit)); /* 2px */
--space-1: calc(0.5 * var(--space-unit)); /* 4px */
--space-2: var(--space-unit); /* 8px */
--space-3: calc(1.5 * var(--space-unit)); /* 12px */
--space-4: calc(2 * var(--space-unit)); /* 16px */
--space-5: calc(2.5 * var(--space-unit)); /* 20px */
--space-6: calc(3 * var(--space-unit)); /* 24px */
--space-8: calc(4 * var(--space-unit)); /* 32px */
--space-10: calc(5 * var(--space-unit)); /* 40px */
--space-12: calc(6 * var(--space-unit)); /* 48px */
--space-16: calc(8 * var(--space-unit)); /* 64px */
--space-20: calc(10 * var(--space-unit)); /* 80px */
--space-24: calc(12 * var(--space-unit)); /* 96px */
}
The 4px Half-Step
Use the 4px sub-grid for fine adjustments only — icon-to-label gaps, input padding, borders. If you find yourself using 4px steps everywhere, your elements are too dense.
When NOT to Use 8px Grid
- Typography line heights: Line heights follow their own rhythm (see Vertical Rhythm below)
- Icon sizes: Icons follow 16/20/24/32 scales, which happen to be 8px-aligned but are driven by optical sizing
- Border radius: Radius is aesthetic, not spatial — 4px, 6px, 8px, 12px are all fine
- Shadows/blur: Shadow values are perceptual, not grid-based
The Modular Scale
A modular scale generates spacing (and type) values using a base and a ratio: value = base * ratio^n. This produces values that feel harmonically related rather than arbitrarily picked.
Common Ratios and When to Use Each
Ratio Name Feel Best For
──────────────────────────────────────────────────────────
1.125 Major Second Very tight, dense Data-heavy dashboards, admin UIs
1.200 Minor Third Comfortable General purpose, content sites
1.250 Major Third Balanced Blogs, marketing, SaaS
1.333 Perfect Fourth Generous Editorial, luxury brands
1.414 Augmented Fourth Dramatic Portfolio, creative agencies
1.500 Perfect Fifth Very spacious Minimal sites, Apple-style
1.618 Golden Ratio Maximum drama Art, fashion, single-product
The Generator — CSS Custom Properties
:root {
--scale-ratio: 1.25; /* Major Third — change this one value */
--scale-base: 1rem; /* 16px */
/* Negative steps (smaller than base) */
--scale-n2: calc(var(--scale-base) / var(--scale-ratio) / var(--scale-ratio));
--scale-n1: calc(var(--scale-base) / var(--scale-ratio));
/* Base */
--scale-0: var(--scale-base);
/* Positive steps (larger than base) */
--scale-1: calc(var(--scale-base) * var(--scale-ratio)); /* 20px */
--scale-2: calc(var(--scale-base) * var(--scale-ratio) * var(--scale-ratio)); /* 25px */
--scale-3: calc(var(--scale-base) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); /* 31.25px */
--scale-4: calc(var(--scale-base) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); /* ~39px */
--scale-5: calc(var(--scale-base) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio) * var(--scale-ratio)); /* ~49px */
}
Major Third (1.25) — Computed Values
This is the most versatile ratio. Here is what the scale produces:
Step Exact Rounded Tailwind Nearest Use Case
────────────────────────────────────────────────────────────────
-3 8.19px 8px 2 Micro gaps
-2 10.24px 10px 2.5 Small icon gaps
-1 12.80px 13px 3 Tight padding
0 16.00px 16px 4 Base unit / body text
1 20.00px 20px 5 H4 / card padding
2 25.00px 25px 6 H3 / section sub-gap
3 31.25px 31px 8 H2 / between groups
4 39.06px 39px 10 H1 / section gap
5 48.83px 49px 12 Page section break
6 61.04px 61px 16 Hero spacing
7 76.29px 76px 20 Maximum spacing
Grid vs. Modular Scale — When to Use Which
Use the 8px grid when:
- Building a design system used by many designers/developers
- Consistency across a large product is paramount
- Working with a team that needs simple, predictable values
- Building data-dense applications (dashboards, admin panels)
Use a modular scale when:
- Building a marketing site or editorial product
- You want a distinctive visual rhythm
- Typography is a primary design element
- Working solo or with a small team that groks the math
In practice, use both: The 8px grid for layout structure, the modular scale for typography and content spacing. Snap modular scale outputs to the nearest 4px value for the best of both worlds.
Vertical Rhythm
Vertical rhythm means all vertical spacing in your layout aligns to a baseline grid. Text, images, padding, margins — everything snaps to multiples of a single line-height unit. This creates the invisible order that makes professional layouts feel "right."
Setting Up Vertical Rhythm
:root {
--baseline: 1.5rem; /* 24px — your line height becomes your grid */
font-size: 16px;
line-height: 1.5; /* = 24px = var(--baseline) */
}
/* Everything is a multiple of the baseline */
h1 {
font-size: 2.5rem;
line-height: calc(3 * var(--baseline)); /* 72px — 3 lines */
margin-top: calc(2 * var(--baseline)); /* 48px */
margin-bottom: var(--baseline); /* 24px */
}
h2 {
font-size: 2rem;
line-height: calc(2 * var(--baseline)); /* 48px — 2 lines */
margin-top: calc(2 * var(--baseline)); /* 48px */
margin-bottom: calc(0.5 * var(--baseline));/* 12px */
}
h3 {
font-size: 1.5rem;
line-height: calc(1.5 * var(--baseline)); /* 36px — 1.5 lines */
margin-top: calc(1.5 * var(--baseline)); /* 36px */
margin-bottom: calc(0.5 * var(--baseline));/* 12px */
}
p {
margin-top: 0;
margin-bottom: var(--baseline); /* 24px — one line */
}
/* Images snap to the grid too */
img {
height: auto;
margin-bottom: var(--baseline);
}
The lh and rlh Units (Modern CSS)
The lh unit equals the computed line-height of the current element. The rlh unit equals the computed line-height of the root element (<html>). These are purpose-built for vertical rhythm and have 94%+ browser support (shipped in all browsers by end of 2023).
html {
font-size: 16px;
line-height: 1.5; /* 1rlh = 24px */
}
/* Spacing that inherently follows vertical rhythm */
article p {
margin-bottom: 1lh; /* One line of spacing */
}
article h2 {
margin-top: 2rlh; /* Two root-level lines above */
margin-bottom: 0.5rlh; /* Half a root-level line below */
}
article blockquote {
padding: 1lh 1.5rem; /* Vertical padding = one line */
margin: 1.5lh 0; /* Vertical margin = 1.5 lines */
}
/* Progressive enhancement for older browsers */
article p {
margin-bottom: 1.5rem; /* Fallback */
margin-bottom: 1lh; /* Modern browsers */
}
When to use lh vs rlh:
lh— Component-level spacing that scales with local font size (cards, form fields, article body)rlh— Site-wide structural spacing that stays consistent regardless of local font size (page sections, nav, sidebar)
Debugging Vertical Rhythm
Add a visual baseline grid overlay to check alignment:
/* Development only — toggle with a class */
.show-baseline {
background-image: linear-gradient(
to bottom,
transparent calc(1rlh - 1px),
rgba(0, 150, 255, 0.15) calc(1rlh - 1px),
rgba(0, 150, 255, 0.15) 1rlh
);
background-size: 100% 1rlh;
}
Layout Primitives — The "Every Layout" System
Heydon Pickering and Andy Bell identified that all web layouts are compositions of a small number of primitives. Each is intrinsically responsive — no media queries needed. Together they replace thousands of lines of bespoke layout CSS.
1. The Stack
Injects consistent vertical space between sibling elements. The single most important layout primitive.
.stack {
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.stack > * {
margin-block: 0;
}
.stack > * + * {
margin-block-start: var(--space, 1.5rem);
}
Modern version with gap (preferred in 2025+):
.stack {
display: flex;
flex-direction: column;
gap: var(--space, 1.5rem);
}
Use for: Article body content, form fields, sidebar nav items, any vertical list of things.
2. The Cluster
Wrapping horizontal groups with consistent gaps. Items flow left-to-right and wrap naturally.
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--space, 1rem);
justify-content: flex-start;
align-items: center;
}
Use for: Tag lists, button groups, icon rows, breadcrumbs, pill filters, social links.
3. The Center
Horizontally centers a max-width container with consistent gutters.
.center {
box-sizing: content-box;
max-inline-size: var(--measure, 60ch);
margin-inline: auto;
padding-inline: var(--gutter, 1rem);
}
Use for: Page content containers, article bodies, any centered column of content.
4. The Sidebar
A two-panel layout where one panel has a fixed (or content-driven) width and the other fills remaining space. Stacks vertically when the container is too narrow.
.with-sidebar {
display: flex;
flex-wrap: wrap;
gap: var(--space, 1rem);
}
.with-sidebar > :first-child {
flex-basis: var(--sidebar-width, 20rem);
flex-grow: 1;
}
.with-sidebar > :last-child {
flex-basis: 0;
flex-grow: 999;
min-inline-size: var(--content-min, 50%);
}
Use for: Sidebar + main content, settings panels, doc sites, filter + results layouts.
5. The Switcher
Shows children side-by-side when space permits, stacks them when it does not. No media queries — the layout decides based on available width.
.switcher {
display: flex;
flex-wrap: wrap;
gap: var(--space, 1rem);
}
.switcher > * {
flex-grow: 1;
flex-basis: calc((var(--threshold, 30rem) - 100%) * 999);
}
When the container is wider than --threshold, children sit side by side (flex-basis becomes a large negative number, so flex-grow takes over). When narrower, flex-basis becomes a large positive number, forcing wrap.
Use for: Feature comparison grids, pricing cards, any N-up layout that should stack on mobile.
6. The Cover
A full-height layout (usually viewport height) with a centered principal element and optional header/footer.
.cover {
display: flex;
flex-direction: column;
min-block-size: 100vh;
padding: var(--space, 1rem);
}
.cover > * {
margin-block: var(--space, 1rem);
}
.cover > :first-child:not(.centered) {
margin-block-start: 0;
}
.cover > :last-child:not(.centered) {
margin-block-end: 0;
}
.cover > .centered {
margin-block: auto;
}
Use for: Hero sections, login pages, error pages, loading screens, splash screens.
7. The Frame
Crops content to an aspect ratio. Essential for images and video.
.frame {
aspect-ratio: var(--ratio, 16 / 9);
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
.frame > img,
.frame > video {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
Use for: Image thumbnails, video containers, avatar circles (1/1 ratio), card hero images.
8. The Grid
An auto-filling responsive grid. No media queries — items fill available space and wrap to new rows.
.auto-grid {
display: grid;
grid-template-columns: repeat(
auto-fill,
minmax(min(var(--min-column-size, 15rem), 100%), 1fr)
);
gap: var(--space, 1rem);
}
Use for: Card grids, product listings, image galleries, feature grids.
9. The Reel
A horizontal scrolling strip (carousel without JavaScript).
.reel {
display: flex;
gap: var(--space, 1rem);
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-color: var(--color-accent) var(--color-bg);
}
.reel > * {
flex: 0 0 var(--item-width, 20rem);
scroll-snap-align: start;
}
Use for: Image carousels, horizontally scrolling card rows, timeline views.
Composing Primitives
The power is in composition. A real page is primitives nested inside each other:
<!-- Center constrains width, Stack spaces children, Grid fills cards -->
<div class="center">
<div class="stack" style="--space: 3rem;">
<header class="cluster">
<h1>Dashboard</h1>
<div class="cluster" style="--space: 0.5rem;">
<button>Export</button>
<button>Settings</button>
</div>
</header>
<div class="auto-grid" style="--min-column-size: 20rem;">
<article class="stack" style="--space: 0.75rem;">...</article>
<article class="stack" style="--space: 0.75rem;">...</article>
<article class="stack" style="--space: 0.75rem;">...</article>
</div>
<div class="with-sidebar">
<nav class="stack" style="--space: 0.5rem;">...</nav>
<main class="stack">...</main>
</div>
</div>
</div>
CSS Grid vs. Flexbox — The 2026 Decision
The old debate is settled. Use both, for different things.
Use CSS Grid For:
- Page-level layout — header, sidebar, main, footer
- Two-dimensional arrangements — rows AND columns matter
- Alignment across siblings — card grids where headings align across cards (use subgrid)
- Known structure — you know the blueprint: "3 columns, sidebar on left"
- Overlap — elements that need to overlap via grid placement
- Dense packing —
grid-auto-flow: densefor masonry-like fills
/* Page layout — always Grid */
.page {
display: grid;
grid-template-columns: var(--sidebar-width, 280px) 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"sidebar header"
"sidebar main"
"sidebar footer";
min-height: 100dvh;
}
Use Flexbox For:
- One-dimensional flow — a row of buttons, a column of nav items
- Content-driven sizing — items sized by their content, not the grid
- Wrapping groups — clusters, tag lists, breadcrumbs
- Single-axis alignment — centering one thing in a container
- Unknown item count — "however many items there are, space them evenly"
/* Navigation — always Flexbox */
.nav {
display: flex;
align-items: center;
gap: var(--space-4);
}
.nav-links {
display: flex;
gap: var(--space-6);
margin-inline-start: auto; /* Push to the right */
}
Use Subgrid For:
Subgrid (97%+ browser support, Chrome 117+, Firefox 71+, Safari 16+) lets nested elements align to their parent grid's tracks. This solves the "card content alignment" problem that plagued CSS for years.
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-6);
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* Card spans 3 parent rows: image, content, actions */
}
/* Now every card's image, content, and action bar align across the row */
The Decision Tree
Need to place items in ROWS and COLUMNS?
├── YES → CSS Grid
│ ├── Children need to align with grandparent grid? → Subgrid
│ └── Layout known ahead of time? → Named grid areas
└── NO → One dimension only
├── Items should wrap? → Flexbox + flex-wrap
├── Items should scroll? → Flexbox + overflow-x (Reel)
└── Items in a line? → Flexbox
Fluid Spacing with clamp()
Hard breakpoints create jarring spacing jumps. Fluid spacing scales smoothly between a minimum and maximum value based on viewport (or container) width.
The Formula
clamp(min, preferred, max)
Where preferred = slope * 100vw + intercept
slope = (maxSize - minSize) / (maxViewport - minViewport)
intercept = minSize - slope * minViewport
Practical Fluid Spacing Scale
This scale goes from 320px viewport (mobile) to 1440px viewport (desktop):
:root {
/* Fluid spacing — scales smoothly from mobile to desktop */
--fluid-space-xs: clamp(0.25rem, 0.18rem + 0.36vw, 0.5rem); /* 4px → 8px */
--fluid-space-s: clamp(0.5rem, 0.36rem + 0.71vw, 1rem); /* 8px → 16px */
--fluid-space-m: clamp(1rem, 0.71rem + 1.43vw, 2rem); /* 16px → 32px */
--fluid-space-l: clamp(1.5rem, 1.07rem + 2.14vw, 3rem); /* 24px → 48px */
--fluid-space-xl: clamp(2rem, 1.43rem + 2.86vw, 4rem); /* 32px → 64px */
--fluid-space-2xl: clamp(3rem, 2.14rem + 4.29vw, 6rem); /* 48px → 96px */
--fluid-space-3xl: clamp(4rem, 2.86rem + 5.71vw, 8rem); /* 64px → 128px */
}
How to Derive These Values
Step-by-step for --fluid-space-m (16px at 320px → 32px at 1440px):
minSize = 1rem (16px)
maxSize = 2rem (32px)
minViewport = 20rem (320px)
maxViewport = 90rem (1440px)
slope = (2 - 1) / (90 - 20) = 1/70 ≈ 0.01429
intercept = 1 - 0.01429 * 20 = 1 - 0.2857 = 0.7143rem
preferred = 0.7143rem + 1.429vw
→ round: 0.71rem + 1.43vw
Result: clamp(1rem, 0.71rem + 1.43vw, 2rem)
One-Up Fluid Pairs (Utopia-Style)
For dramatic spacing jumps (e.g., mobile section gap = 24px, desktop = 64px), skip scale steps:
:root {
/* "s-l" pair: small on mobile, large on desktop */
--fluid-space-s-l: clamp(0.5rem, -0.07rem + 2.86vw, 3rem); /* 8px → 48px */
/* "m-xl" pair: medium on mobile, extra-large on desktop */
--fluid-space-m-xl: clamp(1rem, 0.14rem + 4.29vw, 4rem); /* 16px → 64px */
}
These are ideal for section spacing where mobile needs tighter gaps but desktop can breathe.
Container Query Units for Component Spacing
When a component needs to adapt its spacing to its container (not the viewport), use container query units. Full browser support as of 2025.
.card-container {
container-type: inline-size;
container-name: card;
}
.card {
/* Spacing scales with the card's container, not the viewport */
padding: clamp(0.75rem, 3cqi, 2rem);
gap: clamp(0.5rem, 2cqi, 1.5rem);
}
/* When the container is wide enough, switch layout */
@container card (min-inline-size: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: var(--space-6);
}
}
Container query unit reference:
cqw— 1% of the query container's widthcqh— 1% of the query container's heightcqi— 1% of the query container's inline size (preferred for international support)cqb— 1% of the query container's block sizecqmin— smaller ofcqiorcqbcqmax— larger ofcqiorcqb
Whitespace as a Design Element
Whitespace is not empty space — it is active design material. The amount of whitespace directly communicates hierarchy, grouping, and importance. Increasing whitespace by 50% can boost comprehension by 20%.
Micro vs. Macro Whitespace
Micro whitespace — the space within and between small elements:
- Letter spacing (tracking)
- Line height (leading)
- Padding inside buttons, inputs, chips
- Gap between icon and label
- Space between list items
Macro whitespace — the space between large sections:
- Section padding (top/bottom)
- Margins between content blocks
- Sidebar gutters
- Header/footer breathing room
- The gap between hero and first content section
The Content Density Spectrum
DENSE ◄──────────────────────────────────► SPACIOUS
Bloomberg Gmail GitHub Notion Stripe Linear Apple
Terminal .com
Data-heavy Productivity Balanced Marketing Luxury
Dashboards Tools Sites Brands
Where you sit on this spectrum should be a deliberate choice, not an accident:
- Dense (4px base) — Dashboards, trading platforms, IDE-like interfaces. Every pixel matters. Users are experts who want maximum information density.
- Comfortable (8px base) — SaaS products, productivity tools. Balance between information and breathing room.
- Spacious (12-16px base) — Marketing sites, portfolios, editorial. Whitespace IS the design. Each element has room to breathe and be appreciated.
How Apple Uses Whitespace
Apple uses whitespace aggressively. Typical patterns:
- Hero sections with 120-200px top/bottom padding
- Product images floating in vast empty space (60-70% of the viewport is white)
- Text blocks constrained to 580-650px wide, centered in full-width sections
- Section-to-section gaps of 100-160px
- Minimal navigation (small logo, few links, lots of space between them)
How Stripe Uses Whitespace
Stripe balances information density with elegance:
- Content max-width of ~1080px, centered with generous side padding
- Section padding of 80-120px top/bottom
- Card components with 32-48px internal padding
- Code blocks with 24px padding, 16px gap between code and description
- Feature grids with 32-48px gap between items
How Linear Uses Whitespace
Linear epitomizes the modern SaaS aesthetic:
- Full-bleed sections with 80-100px padding
- Feature descriptions with 48-64px between heading and body
- Screenshots/demos floating in dark backgrounds with 40-60px padding
- Compact navigation with precisely 24px between items
- Modular sections that each feel like a complete "slide"
Component-Level Spacing
The Padding Ratio for Interactive Elements
Buttons, inputs, pills, and chips follow a consistent ratio: vertical padding is roughly 60-66% of horizontal padding. This is close to the golden ratio (0.618) and accounts for how the human eye perceives horizontal vs. vertical space differently.
/* Button padding ratios */
.btn-sm {
padding: 6px 12px; /* 0.50 ratio — compact */
/* Or: padding: 0.375rem 0.75rem; */
}
.btn-md {
padding: 10px 16px; /* 0.625 ratio — standard, near golden */
/* Or: padding: 0.625rem 1rem; */
}
.btn-lg {
padding: 14px 24px; /* 0.583 ratio — generous */
/* Or: padding: 0.875rem 1.5rem; */
}
.btn-xl {
padding: 18px 32px; /* 0.5625 ratio — hero CTA */
/* Or: padding: 1.125rem 2rem; */
}
/* Input fields follow the same principle */
.input {
padding: 10px 14px; /* Slightly more horizontal than buttons */
}
/* Pills / chips — tighter ratio because they are small */
.pill {
padding: 4px 10px; /* 0.4 ratio */
}
Gap as First-Class Spacing
In modern CSS, gap has replaced margin for spacing between siblings inside flex and grid containers. Always prefer gap over margins for sibling spacing.
/* OLD — margin-based (fragile, causes collapse issues) */
.old-list > * + * {
margin-top: 1rem;
}
/* NEW — gap-based (clean, no collapse, works in flex and grid) */
.new-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
Why gap wins:
- No margin collapse issues
- Works identically in Flexbox and Grid
- Single property controls both row-gap and column-gap
- Does not add space before the first or after the last item
- Easier to override with a single custom property
Card Spacing System
Cards are the most common component in web design. Their spacing needs a system:
.card {
--card-padding: var(--space-6); /* 24px default */
--card-gap: var(--space-4); /* 16px between elements */
--card-section-gap: var(--space-6); /* 24px between sections */
padding: var(--card-padding);
display: flex;
flex-direction: column;
gap: var(--card-gap);
}
.card-header {
display: flex;
align-items: center;
gap: var(--space-3); /* 12px icon-to-title */
}
.card-body {
display: flex;
flex-direction: column;
gap: var(--space-2); /* 8px between paragraphs */
}
.card-footer {
display: flex;
gap: var(--space-3);
padding-top: var(--card-gap);
border-top: 1px solid var(--color-border);
margin-top: auto; /* Push to bottom */
}
The Internal-Less-Than-External Rule
A foundational spacing rule from Gestalt psychology: space inside a component should be less than or equal to space outside it. This creates clear visual boundaries without needing borders or backgrounds.
BAD (internal > external):
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ Content │ 8px │ Content │ ← Cards feel merged
│ │ │ │
│ 48px pad │ │ 48px pad │
└──────────────┘ └──────────────┘
GOOD (internal <= external):
┌──────────────┐ ┌──────────────┐
│ │ │ │
│ Content │ 32px │ Content │ ← Cards feel distinct
│ │ │ │
│ 24px pad │ │ 24px pad │
└──────────────┘ └──────────────┘
When the external gap is generous relative to internal padding, each card reads as its own entity. When internal padding exceeds external gap, cards blur together into an amorphous mass.
Page-Level Composition
Max-Width Containers
Why is 1200px the most common max-width? It is not magic — it is the comfortable reading width at standard text sizes on the most common desktop resolution (1440px or 1920px). At 1200px max-width on a 1440px screen, you get 120px of side breathing room. On 1920px, you get 360px — generous but not wasteful.
/* Standard content container */
.container {
width: 100%;
max-width: 75rem; /* 1200px */
margin-inline: auto;
padding-inline: var(--gutter, 1.5rem);
}
/* Narrow for long-form reading (optimal: 45-75 characters per line) */
.container-prose {
max-width: 42rem; /* ~672px ≈ 65ch at 16px body text */
}
/* Wide for dashboards and data-heavy pages */
.container-wide {
max-width: 90rem; /* 1440px */
}
/* Full-bleed for hero sections and backgrounds */
.container-full {
max-width: none;
padding-inline: 0;
}
Common Max-Width Values and Their Use Cases
Width rem Use Case Example Sites
──────────────────────────────────────────────────────────────────────
640px 40rem Narrow prose, centered forms Medium articles
768px 48rem Blog content, documentation MDN, Notion pages
960px 60rem Balanced content + whitespace GitHub READMEs
1080px 67.5rem Marketing content sections Stripe, Linear
1200px 75rem Standard page container Most SaaS products
1440px 90rem Wide dashboards, admin panels Vercel Dashboard
100% 100% Full-bleed backgrounds/heroes Apple product pages
Gutter Calculations
Gutters are the space between the edge of the viewport and your content container. They should be fluid:
:root {
--gutter: clamp(1rem, 3vw, 3rem); /* 16px → 48px */
}
.container {
max-width: 75rem;
margin-inline: auto;
padding-inline: var(--gutter);
}
Full-Bleed Inside Constrained Content
A common pattern: the main content is constrained to 1200px, but some sections (hero, testimonial bands, CTA strips) stretch full-width with colored backgrounds.
/* Method 1: Negative margin + viewport width */
.full-bleed {
width: 100vw;
margin-inline-start: calc(50% - 50vw);
padding-inline: var(--gutter);
}
/* Method 2: Grid-based (cleaner, no overflow issues) */
.page-grid {
display: grid;
grid-template-columns:
[full-start] minmax(var(--gutter), 1fr)
[content-start] min(75rem, 100% - var(--gutter) * 2)
[content-end] minmax(var(--gutter), 1fr)
[full-end];
}
.page-grid > * {
grid-column: content;
}
.page-grid > .full-bleed {
grid-column: full;
}
Method 2 is superior — no horizontal scrollbar issues, no overflow: hidden hacks, and the grid handles gutters automatically.
Sidebar + Content Ratios
Common sidebar-to-content proportions:
Ratio Sidebar Content Feel
────────────────────────────────────────────────
1:3 240px 960px Minimal sidebar (nav only)
1:2.5 280px 920px Standard (most common)
1:2 320px 880px Heavy sidebar (filters, tools)
1:1.5 360px 840px Dual-pane (email, chat)
/* Implementation */
.layout {
display: grid;
grid-template-columns: minmax(240px, 280px) 1fr;
gap: var(--space-6);
}
/* Collapse sidebar on mobile */
@media (max-width: 768px) {
.layout {
grid-template-columns: 1fr;
}
}
Responsive Spacing Strategy
Viewport Units — Which to Use
Unit Meaning Use When
────────────────────────────────────────────────────────────
vh 1% of viewport height Legacy fallback only
vw 1% of viewport width Fluid calculations
dvh 1% of dynamic viewport Mobile-safe full-height (changes with browser chrome)
svh 1% of small viewport Conservative: assumes browser chrome IS showing
lvh 1% of large viewport Generous: assumes browser chrome is NOT showing
For mobile-safe full-height layouts, always use dvh:
.hero {
min-height: 100vh; /* Fallback */
min-height: 100dvh; /* Modern: accounts for mobile browser chrome */
}
When to Use Breakpoints vs. Fluid Scaling
Use fluid scaling (clamp) for:
- Font sizes
- Padding and margins that should grow/shrink smoothly
- Container gutters
- Section spacing
Use breakpoints for:
- Layout structure changes (1 column → 2 columns)
- Showing/hiding elements (mobile menu vs. desktop nav)
- Component variants (stacked card → horizontal card)
Use container queries for:
- Component-level layout changes regardless of viewport
- Reusable components that live in different contexts (sidebar vs. main content)
Breakpoint-Specific Spacing Overrides
When fluid spacing is not enough:
:root {
--section-padding: var(--fluid-space-l); /* Fluid default */
}
/* On very small screens, tighten aggressively */
@media (max-width: 480px) {
:root {
--section-padding: var(--space-6); /* Fixed 24px */
}
}
/* On large screens, let it breathe */
@media (min-width: 1440px) {
:root {
--section-padding: var(--space-24); /* Fixed 96px */
}
}
Real-World Spacing Scales Compared
Tailwind CSS v4 (2025+)
Dynamic scale based on --spacing: 0.25rem (4px):
Class Value Pixels
──────────────────────────────
p-0 0 0px
p-0.5 0.125rem 2px
p-1 0.25rem 4px
p-2 0.5rem 8px
p-3 0.75rem 12px
p-4 1rem 16px
p-5 1.25rem 20px
p-6 1.5rem 24px
p-8 2rem 32px
p-10 2.5rem 40px
p-12 3rem 48px
p-16 4rem 64px
p-20 5rem 80px
p-24 6rem 96px
Every multiple of the base is available (p-7 = 1.75rem = 28px), not just the listed ones.
Material Design 3
Based on 4dp increments:
Token Value
─────────────────────────────
spacing.none 0dp
spacing.extraSmall 4dp
spacing.small 8dp
spacing.medium 12dp
spacing.large 16dp
spacing.extraLarge 24dp
spacing.2xl 32dp
spacing.3xl 48dp
spacing.4xl 64dp
Carbon Design System (IBM)
Linear 8px scale:
Token Value Use
─────────────────────────────────────────
$spacing-01 2px Fine adjustments
$spacing-02 4px Compact padding
$spacing-03 8px Small gaps
$spacing-04 12px Form spacing
$spacing-05 16px Standard spacing
$spacing-06 24px Component groups
$spacing-07 32px Section gaps
$spacing-08 40px Large sections
$spacing-09 48px Page sections
$spacing-10 64px Major divisions
$spacing-11 80px Page-level
$spacing-12 96px Maximum
$spacing-13 160px Hero spacing
Atlassian Design System
Semantic naming with 8px base:
Token Value Description
──────────────────────────────────────────────────
space.0 0px Zero
space.025 2px Hairline
space.050 4px Tightest
space.075 6px Tighter
space.100 8px Tight
space.150 12px Compact
space.200 16px Normal (default)
space.250 20px Comfortable
space.300 24px Spacious
space.400 32px Very spacious
space.500 40px Extra spacious
space.600 48px Section-level
space.800 64px Page-level
space.1000 80px Maximum
Common Spacing Mistakes
Mistake 1: Inconsistent Values
/* BAD — 8 different "small" gaps. Which is the right one? */
.header { gap: 10px; }
.nav { gap: 12px; }
.sidebar { gap: 11px; }
.card { gap: 14px; }
.form { gap: 13px; }
.footer { gap: 9px; }
.modal { gap: 15px; }
.dropdown { gap: 8px; }
/* GOOD — one token, one value, used everywhere */
.header,
.nav,
.sidebar,
.card,
.form,
.footer,
.modal,
.dropdown {
gap: var(--space-3); /* 12px — the one true "small gap" */
}
Fix: Audit your CSS for unique spacing values. A well-designed system uses 8-12 distinct values. If you have 30+, you have a problem.
Mistake 2: Margin Collapse Surprises
Margin collapse happens when vertical margins of adjacent block elements combine into one margin (the larger wins). This only affects block layout — it does NOT happen in Flexbox or Grid.
/* GOTCHA: These margins collapse — you get 32px, not 56px */
.heading { margin-bottom: 24px; }
.paragraph { margin-top: 32px; }
/* Actual gap: 32px (the larger margin wins) */
/* GOTCHA: Parent-child collapse */
.parent { margin-top: 0; }
.child { margin-top: 24px; }
/* The child's margin "escapes" and pushes the PARENT down by 24px */
/* FIX 1: Use gap (no collapse in flex/grid) */
.container {
display: flex;
flex-direction: column;
gap: 24px;
}
/* FIX 2: Use padding on the parent to block parent-child collapse */
.parent { padding-top: 1px; } /* Even 1px blocks collapse */
/* FIX 3: Use overflow to create a block formatting context */
.parent { overflow: hidden; }
/* FIX 4: Use margin in only one direction (Heydon Pickering's "lobotomized owl") */
* + * { margin-top: 1.5rem; } /* Only top margins, no collapse risk */
Mistake 3: Desktop-First Spacing That Breaks on Mobile
/* BAD — 96px section padding looks great on desktop, absurd on 375px mobile */
section {
padding: 96px 48px;
}
/* GOOD — fluid spacing that adapts */
section {
padding: clamp(2rem, 4vw + 1rem, 6rem) var(--gutter);
}
Mistake 4: Over-Spacing That Wastes Real Estate
Not everything needs 80px of breathing room. Over-spacing pushes content below the fold, increases scroll depth, and can make pages feel empty rather than elegant.
/* BAD — every element drowning in whitespace */
h2 { margin-top: 80px; margin-bottom: 40px; }
p { margin-bottom: 32px; }
ul { margin-bottom: 48px; }
/* User scrolls forever to reach the CTA */
/* GOOD — proportional to content importance */
h2 { margin-top: 48px; margin-bottom: 16px; }
p { margin-bottom: 16px; }
ul { margin-bottom: 24px; }
Mistake 5: Using Padding When You Mean Margin (or Vice Versa)
PADDING = space INSIDE the element boundary (between border and content)
MARGIN = space OUTSIDE the element boundary (between element and neighbors)
Use PADDING for:
- Internal breathing room (card content inset)
- Click/tap target expansion
- Background color coverage area
Use MARGIN for:
- Separation between distinct elements
- Push away from siblings
- Page-level section spacing (or use gap)
Use GAP for:
- Space between children of a flex/grid container
- ALWAYS prefer gap over margin for sibling spacing
Mistake 6: Ignoring the 4px Sub-Grid for Small Elements
/* BAD — 8px gap between icon and label feels too wide */
.icon-label { gap: 8px; }
/* GOOD — drop to the 4px sub-grid for tight pairings */
.icon-label { gap: 4px; }
/* Also good for: */
.badge { padding: 2px 6px; } /* 2px top/bottom, 6px sides */
.avatar-sm { width: 24px; height: 24px; }
.divider { margin-block: 4px; }
Debugging "Why Does This Look Wrong?"
The Squint Test
Literally squint at your screen (or step back
…(truncated)