Spacing
The 4px-based spacing system, the semantic categories that alias it, and the principles for picking the right value. Built on proximity: where the eye sees less space, the mind sees more relationship.
Principles
Adapted from Fluent 2's spacing and proximity guidance. Proximity is the headline idea — see the demo below.
- 01
Proximity signals relationship
Elements placed close together read as related. Elements separated by more space read as distinct. Use spacing — not borders or boxes — as the first tool to group content.
- 02
Hierarchy comes from spacing before size
Generous space around an element promotes it; tight space embeds it. Reach for spacing before increasing font size or weight to signal importance.
- 03
Whitespace is content
Density makes scanning hard and signals "everything is equally important," which is rarely true. Default to more whitespace; reduce only when there is a specific reason.
- 04
Consistency creates rhythm
Use the same value for the same purpose every time. Two cards in a row should have identical inset padding; two paragraphs in a sequence should have identical stack gap.
Proximity in action
The same content, at three different stack values. As the gap grows, the items stop reading as a related group.
The ramp
Base unit: 4px. Eleven steps. Token names use the multiplier (e.g., spacing.4 = 4 × 4px = 16px). Click any row to copy its CSS variable.
Semantic categories
Five purpose-driven groups alias the global ramp. Use semantic tokens in components and pages whenever a purpose-fit alias exists. Drop to global spacing.N only when no alias matches.
inset — padding inside a component
Roughly maps to Fluent's “Component spacing.”
| Token | CSS variable | Value | Use for |
|---|---|---|---|
--spacing-inset-xs | 8px | Compact components: badge, tag, chip | |
--spacing-inset-sm | 12px | Small components: icon button | |
--spacing-inset-md | 16px | Default: card, input, button | |
--spacing-inset-lg | 24px | Large containers: modal, panel |
inline — horizontal gap between siblings
| Token | CSS variable | Value | Use for |
|---|---|---|---|
--spacing-inline-xs | 4px | Icon-to-text, very tight inline | |
--spacing-inline-sm | 8px | Button icon-to-label, badge gaps | |
--spacing-inline-md | 16px | Default inline rhythm |
stack — vertical gap between siblings
With inline, this covers what Fluent calls “Pattern spacing.”
| Token | CSS variable | Value | Use for |
|---|---|---|---|
--spacing-stack-xs | 4px | Label to input, very tight stacking | |
--spacing-stack-sm | 8px | Form field vertical spacing | |
--spacing-stack-md | 16px | Default vertical rhythm | |
--spacing-stack-lg | 24px | Section content stacking, heading-to-content |
content.gap & section.gap — layout rhythm
These cover what Fluent calls “Layout spacing.”
| Token | CSS variable | Value | Use for |
|---|---|---|---|
--spacing-content-gap | 16px | Default gap between content blocks (cards in a grid, items in a list) | |
--spacing-section-gap | 48px | Between major page sections (h2 to h2) |
Which token do I reach for?
A four-step flow for picking a value. Most of the time the answer falls out at step 1 or 2.
- 1Is there a semantic alias? Use it.
spacing.inset.mdoverspacing.4. - 2Is the relationship tight, normal, or loose? Pick the corresponding
xs / sm / md / lg. - 3Is the value ≥ 32px? It is probably a layout concern — use
content.gaporsection.gap, notinsetorstack. - 4None of the above fits? Drop to the global ramp (
spacing.N). If the value isn't in the ramp, the design probably needs another look.
Worked example: a card
- Padding inside the card →
spacing.inset.md(16px). - Gap between card title and body →
spacing.stack.xs(4px) — tight, they're closely related. - Gap between body and the action row →
spacing.stack.md(16px) — the action is distinct. - Gap between cards in a grid →
spacing.content.gap(16px). - Gap between a card row and the next section heading →
spacing.section.gap(48px).
Touch targets
Spacing on its own can produce interactive elements that are too small to tap reliably on touch devices. Independent of token values, every interactive element must hit the platform minimums.
Usage guidelines
Use semantic tokens (spacing.inset.md, spacing.stack.lg) in components and pages.
Reach for global spacing.N first. Only drop to it when no semantic alias matches.
Use the same token for the same purpose every time. Two cards in a row should have identical inset padding.
Mix arbitrary values like 14px or 20px. Pick the nearest ramp step.
Use spacing — not borders or boxes — as the first tool to group content.
Add a divider when a tighter stack value would communicate the same grouping.
Using the system
Card with inset padding and internal stack
.card {
padding: var(--spacing-inset-md);
display: flex;
flex-direction: column;
gap: var(--spacing-stack-md);
}
.cardTitle { /* tight to the body */ margin-bottom: var(--spacing-stack-xs); }Form with vertical rhythm
.formField {
display: flex;
flex-direction: column;
gap: var(--spacing-stack-xs); /* label tight to input */
}
.form {
display: flex;
flex-direction: column;
gap: var(--spacing-stack-md); /* fields stacked normally */
}Page-level rhythm
.section {
margin-bottom: var(--spacing-section-gap); /* 48px between H2s */
}
.sectionContent {
display: grid;
gap: var(--spacing-content-gap); /* 16px between blocks */
}