Layout
Spacing and Layout — Synthesis
Overview
A spacing system is a scale with a shared base unit that components and layouts consume to maintain proportional relationships across the UI. Its value is not any particular set of values — it is the discipline of enforcing a single coordinate system so that spacing decisions don't require per-element negotiation.
Base unit: 4px vs. 8px
Most production design systems use either 4px or 8px as the base grid unit. Both are practical. The choice matters less than using one consistently.
4px base (Radix, Material, Atlassian): More granular. Enables steps at 4, 8, 12, 16, 20, 24px — the tighter values (4px, 8px, 12px) are useful for component-internal spacing (icon gaps, badge padding, button icon margins) where 8px is often too large. A 4px base generates more usable steps without requiring half-steps or arbitrary values.
8px base (Carbon, many utility-class systems): Fewer steps, cleaner multiples. 8, 16, 24, 32, 40, 48px covers most layout and component needs. The coarser grid forces designers to commit more decisively — there are fewer values to disagree about.
The practical rule: Use 4px if your UI has dense components with many internal spacing decisions (data tables, form controls, navigation items). Use 8px if the product is more spacious and layout-level spacing dominates. Either way, never define arbitrary values outside the scale — if a value doesn't exist in the scale, that's a signal to round to the nearest step or to add the step.
The scale as a unified system
The spacing scale should connect three levels of the UI as a coherent system:
Component-internal spacing (4–16px range): gaps between an icon and its label, padding inside a button, space between form field label and input. These values are small and precise — the 4px half-steps matter here.
Component-to-component spacing (8–32px range): the gap between a heading and its body text, space between cards in a list, margin around a form group. This is where the rhythm of a UI becomes visible.
Layout-level spacing (16–64px range): section padding, page margins, major whitespace zones. These values create the breathing room that separates content regions and establishes visual hierarchy at the page level.
A scale that only works at one level fails at the others. A 4-step scale (8, 16, 24, 32) is insufficient for component-internal use. A scale with too many steps (Atlassian's 14-step scale from 2px to 80px) gives designers too much freedom and produces inconsistent results without strong convention.
A practical starting scale for a mixed product:
| Step | Value | Typical use |
|---|---|---|
| 1 | 4px | Icon gaps, badge padding, tight insets |
| 2 | 8px | Button padding (vertical), small component gaps |
| 3 | 12px | Component padding (tight), between label and input |
| 4 | 16px | Standard component padding, between list items |
| 5 | 24px | Between components in a section, card padding |
| 6 | 32px | Section gaps, sidebar padding |
| 7 | 48px | Major layout gaps, section vertical padding |
| 8 | 64px | Large section whitespace, hero padding |
Density axis
The density axis governs how tight or generous spacing is throughout a product. It is not a single value — it pervades every spacing decision.
Dense products (data-heavy apps, admin tools, dashboards) need: tighter component padding, smaller gaps between elements, and smaller step values used throughout the scale. A button with 8px vertical padding instead of 16px, a table row at 32px tall instead of 48px.
Spacious products (editorial, onboarding, marketing) need: generous padding, visible whitespace between sections, larger step values as defaults.
Design the density before designing the scale. A scale calibrated for a dense dashboard will feel cramped if repurposed for a marketing page; a spacious scale will waste screen space in a dense application.
For mixed products that need both densities, define a density axis rather than two separate scales:
- A global scaling multiplier applied uniformly (Radix's
scalingprop at 90–110%) - A discrete "compact/default/comfortable" mode that remaps specific spacing tokens
- A set of density-variant component tokens for the highest-frequency components (tables, forms, navigation)
The multiplier approach is simpler to implement; the token-remap approach gives more granular control. Neither is universally correct.
Breakpoint philosophy
Content-first, not device-first. Define breakpoints at the sizes where your specific content needs to reflow — where the text measure becomes too wide, where the navigation no longer fits side-by-side, where the grid needs to collapse. Don't define breakpoints at device sizes (375, 768, 1024) unless those happen to coincide with your content's needs.
Minimize the number of breakpoints. Each additional breakpoint multiplies the design and implementation surface area. Most products need 3–4 breakpoints; more is usually a sign of premature specificity. Start with two (mobile, desktop), add a tablet breakpoint only when the content genuinely needs different treatment at that width.
Spacing should change at breakpoints, not just layout. It is common to restructure layouts at breakpoints (1 column → 2 columns) while leaving spacing unchanged. This is usually wrong — a component that was 24px padded on mobile often needs 32px or 40px on desktop because the surrounding whitespace has expanded. Define spacing tokens that are breakpoint-aware, or use responsive token values.
Responsive spacing approaches:
The simplest: define baseline scale values that are used as-is across all breakpoints, and override specific tokens at breakpoints where needed.
The more systematic: define spacing tokens as fluid values that scale smoothly between a min and max using clamp():
--space-section: clamp(24px, 5vw, 64px);
This eliminates hard breakpoints for spacing while maintaining the scale semantics. Best suited for layout-level spacing tokens; component-internal spacing rarely needs to be fluid.
The never-arbitrary rule
Every spacing value in a component or layout should reference a scale token or be derivable from one. A value of 17px is a design smell — it means a decision was made that bypasses the scale, which will need to be explained and justified every time someone reads the code.
When you feel the pull toward an off-scale value, ask: Is the scale missing a needed step? Is the component's internal padding trying to compensate for something else (wrong base font size, wrong line height)? Fixing the root cause is almost always better than introducing an off-scale value.