skills/web-layout-spacing/SKILL.md
Proper layout and spacing in modern web design — 8px grid systems, modular scales, vertical rhythm, CSS Grid/Flexbox patterns, fluid spacing with clamp(), container queries, and whitespace as design element. Activate on 'spacing', 'layout spacing', 'grid system', 'whitespace', '8px grid', 'vertical rhythm', 'spacing scale', 'gap', 'padding ratio', 'layout primitives', 'responsive spacing', 'clamp spacing', 'container queries layout'. NOT for animation/motion (use motion-design-web), not for color/typography alone (use typography-expert), not for design system creation (use design-system-bootstrap).
npx skillsauth add curiositech/windags-skills web-layout-spacingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Use for:
Do NOT use for:
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.
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
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 */
}
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.
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.
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
: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 */
}
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
Use the 8px grid when:
Use a modular scale when:
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 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."
: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 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)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;
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
The old debate is settled. Use both, for different things.
grid-auto-flow: dense for 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;
}
/* 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 */
}
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 */
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
Hard breakpoints create jarring spacing jumps. Fluid spacing scales smoothly between a minimum and maximum value based on viewport (or container) width.
clamp(min, preferred, max)
Where preferred = slope * 100vw + intercept
slope = (maxSize - minSize) / (maxViewport - minViewport)
intercept = minSize - slope * minViewport
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 */
}
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)
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.
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 of cqi or cqbcqmax — larger of cqi or cqbWhitespace 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 whitespace — the space within and between small elements:
Macro whitespace — the space between large sections:
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:
Apple uses whitespace aggressively. Typical patterns:
Stripe balances information density with elegance:
Linear epitomizes the modern SaaS aesthetic:
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 */
}
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:
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 */
}
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.
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;
}
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
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);
}
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.
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;
}
}
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 */
}
Use fluid scaling (clamp) for:
Use breakpoints for:
Use container queries for:
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 */
}
}
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.
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
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
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
/* 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.
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 */
/* 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);
}
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; }
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
/* 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; }
Literally squint at your screen (or step back 5 feet). When you cannot read the text, your brain sees only shapes and spacing. Ask:
Temporarily add colored backgrounds to all your containers:
/* Debug mode — add to dev stylesheet */
.debug * {
outline: 1px solid rgba(255, 0, 0, 0.2);
}
.debug *:hover {
outline: 2px solid red;
background: rgba(255, 0, 0, 0.05);
}
This reveals:
All modern browsers show margin/padding visually when you inspect an element:
In Chrome DevTools, hover over any element to see its box model visualization overlaid on the page. In Firefox, the Layout tab shows grid and flex overlays with detailed gap visualization.
Open DevTools, go to Console, and run:
// Find all unique spacing values in use
const allStyles = [...document.querySelectorAll('*')].map(el => {
const s = getComputedStyle(el);
return {
el: el.tagName + (el.className ? '.' + el.className.split(' ')[0] : ''),
mt: s.marginTop, mb: s.marginBottom,
pt: s.paddingTop, pb: s.paddingBottom,
gap: s.gap
};
}).filter(s => s.mt !== '0px' || s.mb !== '0px' || s.pt !== '0px' || s.pb !== '0px');
// Count unique values
const values = new Set();
allStyles.forEach(s => {
[s.mt, s.mb, s.pt, s.pb, s.gap].forEach(v => {
if (v && v !== '0px' && v !== 'normal') values.add(v);
});
});
console.log(`Unique spacing values: ${values.size}`);
console.log([...values].sort((a, b) => parseFloat(a) - parseFloat(b)));
Target: 8-15 unique values for a well-designed system. If you see 30+, your spacing is ad-hoc and needs a system.
SYMPTOM LIKELY CAUSE FIX
─────────────────────────────────────────────────────────────────────────────
Elements look "merged together" Internal >= external spacing Increase gap, decrease padding
One section feels "detached" Too much margin above/below Reduce to match siblings
Cards don't align across rows No shared grid tracks Use CSS Grid + subgrid
Text feels cramped Line-height < 1.4 Set line-height: 1.5-1.6
Text feels too airy Line-height > 1.8 Bring down to 1.5-1.6
Page feels "empty" Over-spacing (macro) Tighten section padding
Page feels "cluttered" Under-spacing, no hierarchy Add macro whitespace between groups
Button text feels off-center Unequal optical padding Add 1-2px more to top (caps sit high)
Sidebar and main don't feel related Gutter too wide Reduce to 24-32px
Hero → content transition is jarring Abrupt spacing change Use fluid spacing for section padding
"Magic number" spacing — Pixel values scattered through CSS with no tokens or scale. Every spacing value should reference a token or scale step.
Spacing with invisible elements — Using empty divs or <br> tags for spacing instead of proper margin/padding/gap. Never use markup for presentation.
Percentage padding for vertical space — padding: 10% computes based on the element's WIDTH, not height. This almost never does what you expect for vertical spacing.
Fixed spacing at every breakpoint — Writing padding: 16px then @media (min-width: 768px) { padding: 24px } then @media (min-width: 1024px) { padding: 32px } when clamp(1rem, 2vw + 0.5rem, 2rem) handles all three.
Mixing margin directions — Some elements push with margin-bottom, others with margin-top, some with both. Pick ONE direction (margin-bottom is conventional) and stick with it, or use gap exclusively.
Ignoring the content measure — Lines of text wider than 75 characters are hard to read. Always constrain prose containers to 45-75ch (roughly 38-65rem at 16px body text).
Copying spacing from Figma 1:1 without fluid adaptation — Figma designs are fixed-width. Translating them to CSS without adding fluid scaling produces layouts that only look right at one viewport width.
Using margin on flex/grid children for sibling spacing — Use gap on the parent instead. Margin on children creates maintenance headaches and edge cases (first/last child exceptions).
Spacing tokens that are too granular — 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 pixel tokens. Nobody can distinguish 7px from 8px. Remove values that are not optically distinct (minimum 4px step between tokens).
No spacing documentation — If your design system does not have a published spacing scale with names, values, and usage guidelines, every developer will invent their own spacing, and they will all be different.
Before shipping any layout:
clamp() for smooth scalingtools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.