Spacing, Rhythm & the 8pt Grid
Master the spatial logic that makes interfaces feel intentional — from the mathematical elegance of the 8pt grid to the visual rhythm that guides the eye.
8 min read
Every gap, padding, and size is a multiple of 8px (8, 16, 24, 32…). A single spacing unit makes layouts feel consistent and removes arbitrary, eyeballed values.
The full lesson
Spacing is the silent architect of every interface. Before a user reads a word or clicks a button, their eye is already parsing proximity, grouping, and rhythm — all encoded in the white space between elements. Get spacing wrong and even beautiful components feel amateur. Get it right and even simple components feel considered.
This lesson builds a solid mental model for spatial design. You will learn why the 8pt grid works, how to extend it into a full spacing scale, and how rhythm ties layout to typography. You will also see how to encode all of this as design tokens so your decisions survive handoff intact.
Why Spacing Deserves Its Own System
Most designers start by picking sizes by feel — a bit of padding here, some margin there. That works fine at first. It stops working when you have two dozen components built by three people across six months. At that point your interface has 47 unique spacing values and nobody can explain why.
A spacing system solves this by constraining choices. Instead of an infinite range of pixel values, you work from a small, deliberate scale. Every gap in your UI maps to one value on that scale. The result is visual consistency that users feel even if they cannot name it.
There is also a practical benefit: a shared scale is a shared language. When a designer says “space-4” and an engineer writes spacing.4, they mean the same thing — no redline conversation needed.
The Mathematics Behind the 8pt Grid
The 8pt grid is not arbitrary. It works because 8 is the smallest integer that divides evenly into the most common screen densities. On a 2x display, 8 logical pixels become 16 physical pixels — a whole number. At 3x, 8 becomes 24. Divisibility matters because fractional pixels produce blurry edges on subpixel rendering.
The scale also maps cleanly to typography. Most readable body text sits between 14px and 18px. Line heights for those sizes tend to land on 20px, 24px, or 28px — all multiples of 4 or 8. When your spacing scale shares the same base as your type scale, elements slot into the grid naturally without manual adjustment.
A standard 8pt spacing scale looks like this:
| Token name | Value | Common use |
|---|---|---|
| space-1 | 4px | Icon-to-label gap, tight inline spacing |
| space-2 | 8px | Internal component padding (small) |
| space-3 | 12px | Internal component padding (medium) |
| space-4 | 16px | Default internal padding, list item spacing |
| space-5 | 24px | Card padding, section sub-spacing |
| space-6 | 32px | Between related content groups |
| space-7 | 48px | Between major sections |
| space-8 | 64px | Page-level vertical rhythm, hero padding |
| space-9 | 96px | Above-the-fold hero, full-bleed section gaps |
The 4px half-step (space-1) is important. Some cases genuinely need tighter spacing than 8px — icon gaps, badge padding, compact table rows. Including 4px prevents the temptation to reach for arbitrary values like 5px or 6px.
Building a Token-Ready Spacing Scale
Modern spacing systems are expressed as design tokens (named, reusable values), not hardcoded numbers. Following the W3C DTCG stable format, a spacing token uses $value and $type keys:
{
"spacing": {
"4": { "$value": "4px", "$type": "dimension" },
"8": { "$value": "8px", "$type": "dimension" },
"16": { "$value": "16px", "$type": "dimension" },
"24": { "$value": "24px", "$type": "dimension" },
"32": { "$value": "32px", "$type": "dimension" },
"48": { "$value": "48px", "$type": "dimension" },
"64": { "$value": "64px", "$type": "dimension" }
}
}
In CSS, these become custom properties that your components reference:
:root {
--spacing-4: 4px;
--spacing-8: 8px;
--spacing-16: 16px;
--spacing-24: 24px;
--spacing-32: 32px;
--spacing-48: 48px;
--spacing-64: 64px;
}
.card {
padding: var(--spacing-24);
gap: var(--spacing-16);
}
The token layer means switching between density modes — compact, comfortable, or spacious — is a single token-value swap, not a find-and-replace across every component file.
Do
--spacing-component-padding when components need context-specific values. Keep the primitive scale separate from semantic usage tokens.Don't
padding: 13px or margin-top: 22px. These values exist outside the system and break the visual contract silently.Spacing and Gestalt: Proximity as Meaning
Spacing communicates relationship before content does. This is the Gestalt principle of proximity: elements that are close together are perceived as a group; elements far apart are perceived as separate. Your spacing choices are therefore semantic — they carry meaning, not just aesthetics.
Apply this in practice:
- Within a component, use your smallest steps (4–8px). The label, input, and hint text of a form field should feel like one unit.
- Between related components, use mid-range steps (16–24px). A stack of form fields should feel like a group within a form.
- Between sections, use larger steps (32–64px). The form, a divider, and a summary panel should feel like distinct regions.
- At page level, use your largest steps (64–96px). Hero, features, pricing, and footer each need clear breathing room.
This creates a spatial hierarchy that mirrors your information hierarchy. Users do not consciously read the spacing, but they use it to navigate. When spatial hierarchy breaks — for example, a section heading sitting closer to the paragraph below than to the one above — users slow down and re-read to confirm the grouping.
Padding vs. Margin: The Internal/External Split
A useful rule: padding is what a component owns; spacing between components is what a layout system owns.
Component padding is defined once inside the component’s styles and stays constant. It gives the component internal breathing room no matter where it is placed.
Spacing between components is better handled with gap in CSS Grid and Flexbox, or via layout-wrapper components that own their own spacing logic. This prevents margin-collapse bugs and makes components genuinely portable.
/* Preferred: layout system owns the spacing between siblings */
.section-stack {
display: flex;
flex-direction: column;
gap: var(--spacing-32);
}
/* Components carry only their internal padding */
.card {
padding: var(--spacing-24);
}
Visual Rhythm: Spacing Meets Typography
Rhythm happens when spacing and type agree on a shared unit. In print typography this is called a baseline grid — every line of text aligns to a shared vertical increment, usually the base line-height. On screen, a strict baseline grid is harder to enforce, but the principle still applies: your spacing scale and your type scale should share a common unit so vertical distances feel related rather than random.
Take a typical body text setup: 16px font size with a 1.5 line-height gives a 24px computed line height. Your spacing scale includes 24px. A paragraph’s natural bottom margin of one line-height drops exactly on a grid line. Headers with a 32px line-height also land on the grid. Section spacing at 48px is exactly two line-heights. The interface breathes in multiples of the same unit, and that regularity is what makes layouts feel calm and readable.
When rhythm breaks — for example, a heading with a 28px line-height that does not divide evenly into your spacing scale — you will notice awkward gaps between the heading and the first paragraph. The fix is usually to adjust the line-height to the nearest grid multiple, or to add a deliberate offset that returns to the grid.
Responsive Spacing: Fluid and Adaptive Approaches
Fixed spacing tokens work well on a single viewport, but interfaces live across a wide range of screen sizes. Two strategies handle this.
Adaptive spacing uses different token values at different breakpoints — a compact scale on mobile and a more generous scale on desktop. This maps naturally to container query breakpoints:
.hero {
padding-block: var(--spacing-48);
}
@container (min-width: 768px) {
.hero {
padding-block: var(--spacing-96);
}
}
Fluid spacing uses clamp() to interpolate smoothly between a minimum and maximum value across a viewport range:
.section {
padding-block: clamp(var(--spacing-32), 5vw, var(--spacing-80));
}
Fluid spacing avoids the sudden jump at a breakpoint. The tradeoff is that values are no longer anchored to the 8pt grid at intermediate sizes. For hero sections and large layout containers that tradeoff is worth it.
For component-level spacing — button padding, input padding, badge padding — stick to fixed token values. The viewport variation is too small to matter, and fluid values make individual components harder to reason about in isolation.
The modern default is intrinsic CSS Grid for layout (not a rigid 12-column grid with device-pixel breakpoints at 375/768/1024) combined with component-scoped container queries. This lets each component respond to its container’s size rather than the viewport — a significant upgrade over viewport-only media queries.
Common Spacing Anti-Patterns
These are the mistakes that most often break visual rhythm in production UIs.
Incrementing by 1px. Values like 7px, 11px, or 13px are almost always wrong. They exist because someone nudged an element until it looked right, bypassing the system. Off-grid values break alignment and produce soft edges on certain display densities.
Using margins for sibling spacing instead of gap. Margins interact with adjacent elements in unpredictable ways — collapsing, negative overrides, stacking context issues. Using gap in Grid or Flexbox removes the ambiguity entirely.
Inconsistent internal padding. A button with 10px horizontal padding and 6px vertical padding is off-grid. Use 12px / 8px or 16px / 8px — both land on the scale and render crisply.
Over-spacing mobile layouts. Generous spacing is a desktop luxury. On a 375px viewport, 96px of section padding consumes 25% of the screen height. Halving desktop spacing values at mobile is a reasonable starting point.
Mixing rem and px spacing without intent. If typography is sized in rem and spacing in px, a user who bumps browser font size from 16px to 20px will get larger text that no longer fits its container. Use rem for padding on text-containing elements — buttons, inputs, cards with body copy — so the container breathes with the text.
Spacing in Design Tools and Handoff
In Figma, the 8pt grid maps directly to Auto Layout gap and padding values. Consistent use of Auto Layout means components carry their internal spacing, and layout frames handle external spacing via gap — the same separation-of-concerns as the CSS model described above.
Design token tooling — Tokens Studio, Style Dictionary, or native Figma Variables — lets you reference the same spacing primitives in Figma and in code. When a developer inspects a component in Dev Mode, they see spacing.16 rather than a raw pixel value, and they know exactly which CSS custom property to reach for.
The outdated workflow — eyeballing spacing in a static comp, then writing pixel-value redlines in a separate PDF or Zeplin link — breaks down because the spec drifts from the source file. A token pipeline eliminates the manual translation step: spacing decisions made in Figma become CSS custom properties in the codebase automatically.
Do
Don't