Ask most people what a grid system is for and they'll describe columns: twelve of them, probably, evenly spaced across the page. That's not wrong, but it's the least interesting part of the system. Columns tell you how to divide width. They say nothing about why a card's internal padding, the gap between two cards, and the space above a section heading should have anything to do with each other — and in an interface without a real grid system, they usually don't. One component ships with 14px of padding, another with 15px, a third with 20px, each value chosen in isolation by whoever built that piece, each one defensible on its own and wrong together.
A grid system's actual job is to make every one of those decisions answerable from a single number, so unrelated parts of an interface — a card built in one sprint, a modal built in another, a marketing page bolted on by a different team — end up sharing a rhythm without anyone coordinating by hand. That number is the spacing unit, and it comes before columns, before gutters, before any layout decision at all.
01The unit comes first
Most mature design systems standardize on an 8-point base unit — Google's Material Design popularized it in 2014 and it has since become the default across iOS, Android, and web design systems alike. The reason isn't aesthetic, it's arithmetic: 8 is cleanly divisible by 2 and 4, which matters because interface elements get scaled at multiple device pixel ratios. A value defined in 8pt increments stays a whole number of physical pixels at 1×, 2×, and 3× density — 8pt becomes 8px, 16px, or 24px with nothing left over. Pick an odd base like 7 or 10, and you'll eventually hit a density where a spacing value lands on a half-pixel, which renderers either round unpredictably or blur.
iOS's point system and Android's dp system are both, in effect, already agreeing to this: an 8pt/8dp step is the smallest unit that reliably survives translation across the density buckets both platforms ship. Web interfaces don't have quite the same hard constraint, but there's no reason to invent a different rule when the one every native platform converged on already works, and using it means a design token can move between web, iOS, and Android specs without a conversion step.
Most systems pair the 8-unit with a 4-unit half-step for the cases where a full 8px is too coarse — icon-to-label gaps, border widths, the padding inside a small badge. The rule of thumb: reach for 4 near the smallest, densest elements (icons, chips, input borders), and switch to multiples of 8 for everything at component scale or larger. A spacing value that isn't a multiple of 4 shouldn't exist in the system at all — if 8 and 4 don't produce the exact gap a design calls for, that's a signal to reconsider the gap, not to add a one-off exception.
02Building the spacing scale
The base unit isn't used directly everywhere — it's the seed for a small, named scale that every padding, margin, and gap value in the interface draws from. A typical scale roughly doubles as it climbs, so the jump between steps stays proportionally meaningful instead of the eye having to tell 44px from 48px apart:
| Token | Value | Typical use |
|---|---|---|
--space-0.5 |
4px | icon-to-label gap, hairline offsets |
--space-1 |
8px | tight internal padding, gap between related items |
--space-1.5 |
12px | form field internal padding |
--space-2 |
16px | card padding, default block gap |
--space-3 |
24px | gutter (desktop), mobile section gap |
--space-4 |
32px | tablet section gap, card-grid gap |
--space-6 |
48px | desktop section gap |
--space-8 |
64px | major section break, hero padding |
--space-12 |
96px | page-level rhythm, hero top offset |
Naming the tokens by multiplier rather than by pixel value —
--space-2 rather than --space-16 — is what
lets the whole scale re-derive itself later. Change the root unit from
8px to 8.5px for a denser platform and every token shifts
proportionally without a single call site changing.
03Columns and gutters are a ratio, not a fixed number
Twelve columns is the near-universal default for the same reason 8 is the near-universal spacing unit: divisibility. Twelve divides evenly by 2, 3, 4, and 6, so a layout can split into halves, thirds, quarters, or sixths without any column ever getting cut mid-way. A 10-column grid only divides cleanly by 2 and 5 — fine until a designer wants a three-column layout, at which point the grid simply can't express it.
The gutter — the gap between columns — should be pulled from the same
spacing scale as everything else, not chosen independently. A common
pairing is a 24px gutter at desktop (--space-3) narrowing
to 16px on mobile (--space-2), so the same 12-column
structure feels proportionally tighter on a smaller viewport without
needing a separate rule.
Margin and gutter are not the same number, even though it's tempting to make them match for simplicity. A gutter separates two columns that both belong to the content; a margin frames that content against the edge of the viewport. Those are different jobs, and they scale differently — a margin usually needs to grow faster than a gutter as viewport width increases, because its job is proportional framing, not internal spacing. A common pattern: gutters step through 16px → 24px as the viewport grows, while margins step through 16px → 24px → 64px → 96px across the same breakpoints.
With those three numbers — container width, column count, gutter — a fixed column's width is just algebra:
column_width = (container_width − (columns − 1) × gutter − 2 × margin) / columns
That formula is worth knowing even though almost nobody hand- calculates it anymore, because it's exactly what CSS Grid and Flexbox are doing for you — and understanding it is what lets you tell, at a glance, whether a design's numbers actually add up to whole pixels or are quietly relying on the browser to round.
04Making the grid fluid
In CSS, the calculation above is rarely written out — fr
units do it automatically. A 12-column grid with an 8-unit-based
gutter is just:
.grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: var(--space-3); /* 24px */
max-width: 1280px;
margin-inline: auto;
padding-inline: var(--space-4); /* 32px, the margin */
}
The minmax(0, 1fr) instead of a bare
1fr matters more than it looks: a bare
fr track has an implicit minimum size equal to its
content, so a column holding an un-breakable long word or a wide image
can force the whole grid wider than its container. Wrapping it in
minmax(0, 1fr) overrides that minimum to zero and lets
the track actually shrink to the space available.
For content that doesn't need to align to the full 12-column structure
— a tag list, a card grid with no fixed count —
auto-fit combined with minmax() gives a grid
that reflows its own column count instead of wrapping awkwardly at a
fixed number:
.card-grid {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(240px, 1fr)
);
gap: var(--space-4); /* 32px */
}
This still respects the underlying rhythm — the gap is still a scale token — it just stops pretending every piece of content needs to know about the site-wide column count to lay out correctly.
05Vertical rhythm, the forgotten axis
Everything above governs horizontal space. Vertical space gets far less attention, which is exactly why it's usually where a grid system quietly falls apart — a page can have flawless columns and still feel uneven if the spacing above and below headings doesn't follow a rule.
The key distinction most systems miss: line-height and block spacing are not governed by the same unit. Block spacing — the gap between a paragraph and the next heading, between cards, between sections — should snap to the 8px spacing scale above. Line-height operates at a finer grain, because text needs more granular control to stay legible at different sizes. A useful sub-rule: keep line-height values as multiples of 4px rather than forcing them onto the 8px scale.
| Role | Font size | Line-height |
|---|---|---|
| Body | 16px | 24px (1.5) |
| Small / caption | 13px | 20px |
| H3 | 20px | 28px |
| H2 | 24px | 32px |
| H1 | 36px | 44px |
With line-heights fixed to 4px steps, the margin above and below each
block of text can then be set to whole multiples of the 8px scale
without ever landing on a fractional pixel — a 24px-line-height
paragraph followed by a 24px (--space-3) gap keeps every
baseline on the page falling on an 8px interval, which is what
actually reads as "aligned" even though no one consciously notices
individual baselines.
Why this is worth the extra rule: a page where every heading's margin is a slightly different, content-fitted number looks fine in isolation and restless in aggregate — the eye can't quite say what's wrong, but nothing feels like it belongs to the same document. Constraining line-height and block spacing to two small, related scales is what removes that restlessness without anyone having to eyeball it section by section.
06Where rhythm breaks in practice
A spacing system rarely fails because someone rejects it outright — it erodes gradually, usually through one of four patterns:
- Compounding drift in nested components. A card with 16px padding placed inside a section with 32px padding, inside a page with 64px padding, looks intentional. The same stack with 15px, 30px, and 60px — each individually "close enough" — compounds into a page where nothing quite lines up with anything else, and no single number is obviously wrong.
- Mixing 4px and 8px inconsistently. The 4px half-step exists for small, dense elements — not as a general escape hatch. Once 4px increments start appearing in section- level spacing "because it fit better," the scale stops being a scale and becomes a suggestion.
- Assets with built-in padding. An icon exported with 2px of invisible padding baked into its SVG viewBox will never sit flush against the grid, no matter how correctly the surrounding component is spaced — the misalignment is inside the asset, not the layout.
- Breakpoints that change the base unit instead of the multiplier. Mobile layouts should use smaller multiples of the same 8px unit (2×, 3×, 4× instead of 4×, 6×, 8×), not a different base unit altogether — otherwise nothing about the desktop rhythm transfers to mobile, and the two are effectively unrelated design systems that happen to share a codebase.
07A worked example
A simple card makes the whole system concrete. Every number below is pulled directly from the scale — none are chosen to "look right" in isolation:
.card {
padding: var(--space-2); /* 16px outer padding */
display: flex;
flex-direction: column;
gap: var(--space-1); /* 8px between icon row and title */
}
.card-icon-row {
display: flex;
align-items: center;
gap: var(--space-0-5); /* 4px icon-to-label gap */
}
.card + .card {
margin-top: var(--space-3); /* 24px between stacked cards */
}
08Cheat sheet
- Pick one base unit — 8px, with a 4px half-step for small, dense elements. Every spacing value in the system is a multiple of one of those two numbers.
- Use 12 columns for anything that needs clean halves, thirds, quarters, or sixths. Pull gutters from the spacing scale, not a separate number.
- Margin and gutter are different tokens — margin frames the viewport edge, gutter separates columns, and they should scale at different rates.
- Snap line-height to 4px steps; snap block spacing (margins between paragraphs, cards, sections) to the 8px scale.
-
column_width = (container − (n − 1) × gutter − 2 × margin) / n— know the formula even iffrandminmax()compute it for you. - Mobile breakpoints should use smaller multiples of the same unit, never a different base unit.