TL;DR
- A spacing system uses a consistent scale (e.g., 4px base) instead of arbitrary values
- The 4-point grid (4, 8, 12, 16, 24, 32, 48, 64) covers 95% of UI spacing needs
- Use CSS custom properties or utility classes to enforce consistency
- Fluid spacing with clamp() lets your spacing scale responsively without breakpoints
Why Spacing Systems Matter
Open any codebase without a spacing system and you'll find: padding: 13px, margin-top: 17px, gap: 22px. Every value is a one-off decision. The result? A UI that looks subtly "off" — inconsistent rhythm that users feel but can't articulate.
A spacing system gives you a limited set of values to choose from. Constraints breed consistency, and consistency breeds quality.
The 4-Point Grid
The most common approach: every spacing value is a multiple of 4px. Why 4? It's divisible by 2 (for half-steps), works well at common screen densities, and gives you a fine-enough grain for UI work.
:root {
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
--space-24: 6rem; /* 96px */
--space-32: 8rem; /* 128px */
}
Note the non-linear progression. Small values increment by 4px, larger values skip more aggressively. This matches how we perceive space — the difference between 4px and 8px is noticeable; the difference between 64px and 68px is not.
Applying the Scale
Component Internal Spacing
.card {
padding: var(--space-6); /* 24px */
border-radius: var(--space-3); /* 12px */
}
.card-title {
margin-bottom: var(--space-2); /* 8px */
}
.card-body {
margin-bottom: var(--space-4); /* 16px */
}
Layout Spacing
.section {
padding-block: var(--space-16); /* 64px top and bottom */
}
.grid {
gap: var(--space-6); /* 24px between grid items */
}
.stack > * + * {
margin-top: var(--space-4); /* 16px between stacked elements */
}
The Stack Pattern
One of the most useful spacing patterns — the "lobotomized owl" selector:
/* Every direct child except the first gets top margin */
.stack > * + * {
margin-top: var(--space-4);
}
/* Variant with different sizes */
.stack-sm > * + * { margin-top: var(--space-2); }
.stack-md > * + * { margin-top: var(--space-4); }
.stack-lg > * + * { margin-top: var(--space-8); }
This creates consistent vertical rhythm without needing to add classes to every child element.
Fluid Spacing with clamp()
Fixed spacing values can feel too tight on mobile or too loose on desktop. clamp() lets spacing scale with the viewport:
:root {
/* Min: 16px | Preferred: 2.5vw | Max: 32px */
--space-fluid-sm: clamp(1rem, 1.5vw + 0.5rem, 2rem);
/* Min: 32px | Preferred: 5vw | Max: 64px */
--space-fluid-md: clamp(2rem, 3vw + 1rem, 4rem);
/* Min: 48px | Preferred: 8vw | Max: 128px */
--space-fluid-lg: clamp(3rem, 5vw + 1.5rem, 8rem);
}
.hero {
padding-block: var(--space-fluid-lg);
}
.section {
padding-block: var(--space-fluid-md);
}
💡 Use the Clamp Calculator to generate clamp() values visually, or the Fluid Type Scale for fluid sizing across breakpoints.
Spacing in Design Tokens
If you're building a design system, expose spacing as tokens:
/* tokens.css */
:root {
/* Primitive tokens */
--size-4: 0.25rem;
--size-8: 0.5rem;
--size-16: 1rem;
--size-24: 1.5rem;
--size-32: 2rem;
/* Semantic tokens */
--spacing-component-padding: var(--size-16);
--spacing-component-gap: var(--size-8);
--spacing-section-padding: var(--size-32);
--spacing-page-margin: var(--size-24);
}
Best Practices
- Do choose a base unit (4px or 8px) and stick to it — consistency is the point
- Do use semantic spacing tokens (--spacing-card-padding) on top of primitive values
- Do use gap for flex/grid spacing instead of margins
- Do consider fluid spacing for responsive layouts
- Don't use arbitrary values like 13px or 17px — round to your nearest scale step
- Don't define 20+ spacing values — 8-10 is plenty for most projects
- Don't use margin for spacing between siblings when gap is available
Common Mistakes
- Too many values — If your scale has 4, 6, 8, 10, 12, 14, 16... you might as well not have a scale. Skip values intentionally
- Mixing units — Pick rem or px for your tokens and stay consistent. rem is better for accessibility
- Forgetting vertical rhythm — horizontal spacing is easy; vertical rhythm between sections and content blocks needs the same discipline
- Not accounting for borders — a 1px border adds to the visual spacing. Use box-sizing: border-box and account for it
Tools to Explore
- Spacing Calculator — generate a complete spacing scale from a base value
- Fluid Calculator — create responsive fluid scales
- Clamp Generator — build clamp() values for fluid spacing and typography