zudo-css-wisdom
GitHub repository

Type to search...

to open search from anywhere

Spacing Philosophy: Proximity Grouping

Space between groups must clearly exceed space within a group, or the layout reads as the wrong hierarchy — no matter how "correct" each individual value is.

The Problem

Spacing communicates structure. Readers do not parse a page by reading its DOM tree — they parse it by eye, and the eye groups elements by how close together they are. This is the Gestalt law of proximity: elements placed near each other are perceived as related; elements placed farther apart are perceived as separate.

Token systems and lint rules — see Design Token Lint and Two-Tier Size Strategy — enforce that spacing values come from an approved scale. They cannot enforce that two adjacent values, placed next to each other, produce a legible grouping. A layout can use nothing but valid tokens and still read as the wrong hierarchy, because the failure is not in any single value — it is in the ratio between the gap within a group and the gap between groups.

Consider a spacing scale with steps at 8px, 12px, 16px, 24px, and 32px. Every one of these is a "correct" token. But using 12px for the space between related items and 16px for the space before the next section is a mistake the lint layer will never catch: 16px is only 1.33× larger than 12px, which is too weak a signal to read as a boundary. The eye blurs the two groups into one.

The Solution

Make the gap between groups clearly larger than the gap within a group — as a rule of thumb, at least roughly double. The absolute values matter less than the ratio between them. Two spacing values are both valid tokens and still describe two different structures depending on how they relate to each other.

On a dense scale, make the rule mechanical rather than a judgment call. Find the ×2 ladders inside your scale — a scale with steps 4/6/8/12/16/24/32/48 contains two, 4→8→16→32 and 6→12→24→48 — and pick both gaps of any boundary from the same ladder, one rung apart. Same ladder, one rung up is exactly 2×, by construction. Adjacent scale steps (12 and 16, 16 and 20) are for different jobs in different places, not the two sides of one grouping boundary. In one measured migration of roughly 3,000 spacing usages, every boundary whose gaps were authored was fixed by re-pairing alone — no scale step renamed or deleted. Boundaries whose within-group rhythm was intrinsic needed a different mechanism; see the next section, and Spacing Architecture for Dense Desktop-Class Web Apps.

Ratio Over Absolute Value — Same Token Scale, Different Grouping

Both columns use the same 12px value for spacing within the group of three products. The left column pairs it with a 16px gap before the footer — a valid token, but too close to 12px to read as a boundary, so the footer looks like a fourth member of the list. The right column pairs the same 12px with 32px — the footer now clearly reads as separate from the product group. No value here is "wrong" in isolation. The ratio is what decides the reading.

For the mechanics of producing these gaps — gap versus margin, and when each applies — see gap vs margin. This article is about deciding how much space to put on each side of a boundary; that one is about which property creates it.

When the within-group gap is not yours to set

The ratio rule assumes you control both gaps. Sometimes the within-group rhythm is intrinsic — the line-height of stacked text inside a card, the natural rhythm of an icon+label row. You cannot shrink it without harming readability, and out-ratioing it (2× a 24px line rhythm = 48px between every card) wastes the space the layout exists to conserve.

When that happens, stop fighting with proximity and switch Gestalt principles: use common region. Give each group a bounded box — border, radius, fill — and a modest gap between boxes. A bounded region groups its contents regardless of internal rhythm, so a 16px inter-card gap reads cleanly against a 24px internal line rhythm that would have required 48px by ratio alone.

The diagnostic: if "fix the ratio" implies either shrinking text line-height or doubling a gap that is already generous, the layout is asking for a bounded region (common region), not more spacing. One measured case — a timeline whose between-card gap (~25px) matched its within-card line rhythm (~24px), fusing eight cards into one list — was resolved with the internal rhythm left byte-identical, adding only border, radius, and a 16px gap.

A Separator Has No Side of Its Own

HTML defines <hr> as a thematic break — nothing more. The spec does not say which section the line belongs to, how much space it deserves, or whether it opens what follows or closes what precedes. The element supplies a line; the spacing around it supplies the meaning. This makes separators the easiest place for proximity grouping to go wrong, because the markup feels self-explanatory while the rendering says something else.

The failure that motivated this article was exactly this simple:

The Disappearing Divider — hr Gaps Tighter Than the Paragraph Rhythm

In the broken version the paragraphs sit 24px apart, but the <hr> carries only 8px above and below. The total gap across the "separator" is smaller than the gap between related paragraphs — so the two paragraphs it was supposed to divide read as more connected than any other pair on the page, and the line itself reads as text decoration. In the fixed version the paragraph rhythm is untouched; only the <hr> margins grew to 48px — double the within-group gap — and the page falls into two clean groups. Same ratio rule as above, applied across a divider.

Decide the separator's role, then space it

Because the element carries no meaning of its own, the first step is a decision, not a value: what do you want this separator to be?

Intended roleSpacing shapeTypical use
Symmetric boundaryEqual, large space on both sides (≥2× the within-group gap)Thematic break between prose blocks
Opener — binds forwardLarge space before, small space afterA rule that introduces the next section, like a heading rule
Closer — binds backwardSmall space before, large space afterA rule that closes a block — a totals line, a signature rule

All three are legitimate. What is not legitimate is not choosing: a separator whose two gaps are accidental — near-equal-and-small, or produced by whatever margins happened to collapse — reads as a random line. Whichever role you pick, the ratio rule governs it: the separating side(s) must clearly exceed the within-group gap, and a binding side (if any) must be clearly smaller.

The border-t + padding-top trap

The opener role has a common accidental inversion. Tailwind's border-t + pt-* pattern applies the top border and the top padding to the same element — the section that comes after the divider:

/* The line sits at the very top of the box.
   padding-top lands BELOW the line, not above it. */
.section-next {
  border-top: 1px dashed hsl(30, 15%, 55%);
  padding-top: 2rem;
}

The border is drawn at the top edge of the box; padding-top is space inside the box, below that line. If nothing supplies space before the box (no margin-top on it, no margin-bottom on what precedes it), the line sits flush against the previous section — visually joining it — while the generous padding pushes the next section's content away from its own separator.

The border-top + padding-top Trap

In the broken version the line has almost no space above it, so it visually clumps with the previous paragraph — the two read as one group, and "Next section starts here" floats disconnected below its own separator. In the fixed version the large margin-top on the divider pushes the line away from the previous section, and the small padding-top keeps the next section's text close beneath the line. The line now reads as intended: the opener of the next section. The trap generalizes — whenever a separator misreads, check which side of the line the visible space physically landed on before touching any values.

The Squint Test

A fast, reliable review heuristic: blur your vision (or literally squint) at the layout and ask which elements clump together. Fine details — text content, icon shapes, exact colors — disappear under blur. What remains is pure spatial grouping: the same signal a reader's peripheral vision and first-glance scan pick up before they read a single word.

Compare the result against the intended outline. If a boundary element — a divider, a heading, a label — clumps with the wrong neighbor, the fix is to adjust the gap ratio around it, not to restyle the element itself. A divider that reads as attached to the wrong section is a spacing bug, not a border-color or line-weight bug.

In an app with a user-controlled display scale (see Display Scale Strategy), run the squint pass at the scale extremes too — 0.75× and 1.5×/2.0×. Fractional scales surface rounding and hairline bugs that are invisible at 1.0×: sub-2px smears, half-pixel bars, and components never wired to the multiplier drifting out of alignment with everything around them.

Squint Test — Toggle Blur to Check Grouping

Toggle the checkbox above. At full clarity, "24 results" reads as ordinary text below a toolbar. Blurred, the large gap above it and its lack of a background box read clearly as "not part of the toolbar group" — the grouping intent survives the blur. If it did not — if the blurred shapes fused into a single blob — that would be the signal to widen the gap between groups.

Quick Reference

ScenarioTechnique
Related items in a list or toolbarSmall, uniform gap (gap on the container)
Boundary between unrelated sectionsGap roughly 2× or more the within-group gap
Any separator (<hr>, border, label)Decide its role first — symmetric boundary, opener, or closer — then set the spacing to express it
Separator that must read as a boundarySpace on the separating side(s) clearly larger than the within-group gap — never smaller
Reviewing a shipped layout for grouping errorsSquint/blur the page; check clumping against the intended outline
Auditing spacing values for hierarchy bugsCompare the ratio between adjacent gaps, not each value in isolation

Common AI Mistakes

  • Treating "valid token" as "correct usage." A spacing scale only constrains which values are allowed, not which value belongs at which boundary. Picking adjacent scale steps (e.g., 12px and 16px) for within-group and between-group spacing produces a legal but illegible layout.

  • Giving a separator less space than the content rhythm around it. If the gaps across an <hr> are smaller than the paragraph-to-paragraph gaps, the "separator" makes its two sides read as more related than ordinary paragraphs — the original failure that motivated this article.

  • Applying border-t + pt-* to the section that follows a divider without adding margin-top before it. The padding lands below the line, not above it, so an intended opener silently becomes a closer for the previous section.

  • Reviewing spacing by checking each value against the design system, not by looking at the rendered result. A lint pass or token audit cannot catch a proximity-grouping error — only visual inspection (ideally the squint test) can.

  • Increasing a single gap in isolation to "fix" a spacing complaint. If a boundary reads wrong, the fix is the ratio between the two adjacent gaps, not an arbitrary bump to one value.

  • Assuming a divider element is self-explanatory regardless of surrounding space. HTML defines <hr> only as a thematic break — it communicates nothing about which section it belongs to on its own. The spacing around it does that work, and which role it plays (boundary, opener, closer) is the author's decision, not the element's.

When to Use

Apply the ratio rule

Whenever a layout has more than one level of grouping — a list of items inside a card, a card inside a page section, a section inside a page. Check that each level's "between" gap is clearly larger than the level below it's "within" gap.

Run the squint test

After any layout is built or restyled, before shipping. It is a fast, tool-free way to catch grouping errors that a token/lint audit will not surface, because it checks the relationship between values, not the values themselves.

Watch the border-t + pt-* pattern specifically

Any time a divider or top border is applied to the element that starts a new section rather than to a dedicated separator element. Confirm which side of the line the visible space actually lands on.

Revision History

CreatedUpdated