Spacing tokens: fewer values, better layouts
A short spacing scale does more for a layout than any grid. How I choose the values, name them, apply them with gap, and clean up a codebase full of one-off margins.

Search an older codebase for margin and padding values and you will usually find thirty or forty distinct numbers. Some are 15px because a design file said 15. Some are 22px because 20 looked slightly wrong next to a 24 that was itself a guess. None of them is a mistake on its own. Together they produce layouts that feel faintly uneven, where nothing lines up and every new component invents its own rhythm.
A spacing scale is a short list of allowed values. It sounds like a restriction. In practice it is the fastest way to make a layout look considered.
Choosing the values
I start from a 4px base, because it divides cleanly into most type sizes and line heights and lands on whole pixels at common zoom levels. Then I pick values that grow faster than linearly, because the difference between 4 and 8 is very visible and the difference between 56 and 60 is not.
:root {
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
--space-9: 96px;
--space-10: 144px;
}
Ten values covers almost everything: icon gaps at the bottom, section breaks at the top. Each step is roughly 1.5 times the previous one past 16px, which keeps adjacent values distinguishable. If two neighbours look the same in use, the scale has one too many.
Naming by size, not by use
It is tempting to name tokens after where they go: --card-padding, --section-gap. Those names feel helpful for a month. Then the card needs a compact variant, the section gap differs on the home page, and you end up with --card-padding-compact set to the same value as --list-gap.
Positional names (--space-5) or size names (--space-l) describe the value and leave the decision to the component. Components can still alias them locally if it helps readability:
.card {
--card-pad: var(--space-5);
padding: var(--card-pad);
}
The alias lives with the card. The global list stays short.
The one rule that matters
Space inside a group should be smaller than space between groups. That is the whole of the Gestalt principle of proximity as it applies to layout, and most spacing bugs are violations of it: a heading equally far from the paragraph above and the paragraph below, so it belongs to neither; a form label closer to the previous field than to its own.
With a scale, the fix is mechanical. Pick a step for the inside, and use a step at least two positions higher for the outside. Label to field: --space-2. Field to next label: --space-5. Heading to its text: --space-4. Section to section: --space-9.
Prefer gap to margins
Margins on children leak. A component that sets margin-bottom on itself makes an assumption about whatever comes next, and the last child usually needs an override. Flexbox and grid gap put the spacing decision on the parent, where it belongs, and never adds space after the last item.
.stack {
display: flex;
flex-direction: column;
gap: var(--stack-gap, var(--space-4));
}
One small utility like this, with the gap passed in as a custom property, replaces most of the margin rules I used to write. For running prose, where you do not control the markup, the * + * selector with margin-block-start remains the practical choice.
Fluid spacing, carefully
Large values should shrink on small screens; 144px between sections on a phone is most of a screen of nothing. I make only the top three or four steps fluid, and leave the small ones fixed, because a 4px gap that scales to 3.2px helps nobody.
:root {
--space-8: clamp(40px, 3rem + 2vw, 64px);
--space-9: clamp(56px, 3.5rem + 4vw, 96px);
--space-10: clamp(80px, 4rem + 6vw, 144px);
}
Cleaning up an existing codebase
You do not need a rewrite. A rough process that has worked for me:
- Grep for every
margin,paddingandgapvalue and count them. A small script that outputs value and frequency is enough. - Map each value to its nearest token. Most will round cleanly: 15 to 16, 22 to 24, 30 to 32.
- Replace in batches by component, and look at each one in the browser. Some rounding will change a layout slightly; nearly always it looks better.
- Add a lint rule that flags raw pixel values in spacing properties, so new one-offs need a reason.
The values that refuse to round are worth a closer look. They are usually compensating for something else: a line height that adds invisible space above text, an image with a transparent border, a component that was nudged to line up with a sibling. Fix the cause and the odd number goes away.
What you get back
Fewer decisions per component, faster reviews, and layouts where things line up without anyone measuring. Rhythm comes from repetition, and a short scale repeats by design.