JM Family Design System

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

spacing.stack.xs (4px)
Title
Subtitle
Description
Reads as one group.
spacing.stack.md (16px)
Title
Subtitle
Description
Reads as related but distinct.
spacing.section.gap (48px)
Title
Subtitle
Description
Reads as separate items.

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.

Rule. All spacing values used in the system come from this ramp. If a value isn't in the ramp, either pick the nearest step or revisit the design — there is almost always an alignment reason behind the original choice.

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.”

card content
TokenCSS variableValueUse for
--spacing-inset-xs8pxCompact components: badge, tag, chip
--spacing-inset-sm12pxSmall components: icon button
--spacing-inset-md16pxDefault: card, input, button
--spacing-inset-lg24pxLarge containers: modal, panel

inline — horizontal gap between siblings

iconlabelmore
TokenCSS variableValueUse for
--spacing-inline-xs4pxIcon-to-text, very tight inline
--spacing-inline-sm8pxButton icon-to-label, badge gaps
--spacing-inline-md16pxDefault inline rhythm

stack — vertical gap between siblings

With inline, this covers what Fluent calls “Pattern spacing.”

TokenCSS variableValueUse for
--spacing-stack-xs4pxLabel to input, very tight stacking
--spacing-stack-sm8pxForm field vertical spacing
--spacing-stack-md16pxDefault vertical rhythm
--spacing-stack-lg24pxSection content stacking, heading-to-content

content.gap & section.gap — layout rhythm

These cover what Fluent calls “Layout spacing.”

Section A
Section B
TokenCSS variableValueUse for
--spacing-content-gap16pxDefault gap between content blocks (cards in a grid, items in a list)
--spacing-section-gap48pxBetween 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.

  1. 1
    Is there a semantic alias? Use it. spacing.inset.md over spacing.4.
  2. 2
    Is the relationship tight, normal, or loose? Pick the corresponding xs / sm / md / lg.
  3. 3
    Is the value ≥ 32px? It is probably a layout concern — use content.gap or section.gap, not inset or stack.
  4. 4
    None 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.

44 × 44 px
iOS & Web
48 × 48 px
Android
If a button looks smaller — say a 16×16 icon-only glyph — inflate its tap area with padding or a transparent hit zone. Touch-target accessibility overrides aesthetic spacing decisions.

Usage guidelines

Do

Use semantic tokens (spacing.inset.md, spacing.stack.lg) in components and pages.

Don't

Reach for global spacing.N first. Only drop to it when no semantic alias matches.

Do

Use the same token for the same purpose every time. Two cards in a row should have identical inset padding.

Don't

Mix arbitrary values like 14px or 20px. Pick the nearest ramp step.

Do

Use spacing — not borders or boxes — as the first tool to group content.

Don't

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 */
}