Building a Fluid Spacing Scale with clamp() and Container Units

Spacing is usually the last thing to go fluid. Teams adopt clamp() for headings, then leave margins, padding, and gaps frozen at fixed rem values that look cramped on large layouts and bloated on small ones. This guide builds a complete fluid spacing scale as design tokens, using clamp() for the interpolation and cqi container units so spacing tracks the component rather than the whole window. It belongs to Fluid Typography with clamp() within Mastering Container Queries & Responsive Layouts, and it reuses the same min/preferred/max anatomy that drives fluid type, applied to space instead of size.

Why container units beat viewport units for spacing

A spacing scale exists to create consistent rhythm. If the rhythm is keyed to the viewport with vw, every component inherits the page width as its reference, so a card in a narrow sidebar gets the same generous padding as a hero spanning the full page. That breaks the intent: the card looks padded out of proportion to its own size. Container units fix this. cqi resolves against the inline size of the nearest ancestor that declares container-type, so a component's spacing grows with the component. Drop the same card into a 280px rail or a 900px main column and its internal gaps stay proportional in both, with no context-specific overrides.

The accessibility rule from typography carries over unchanged: the min and max bounds of each clamp() must be rem, never px or a bare container unit, so a user who zooms or raises their OS font size gets correspondingly larger spacing and never a collapsed gap. Only the preferred middle term should hold the cqi slope. Because everything resolves in CSS during layout, there is no JavaScript and no resize listener — the spacing recalculates as part of the normal layout pass, the same way fluid type does.

There is a deliberate split worth naming. Page-level rhythm — the outer page gutters, the vertical spacing between major sections — can legitimately use vw, because there the viewport is the relevant reference. Component-internal rhythm should use cqi. A well-built token set exposes both and lets each context pick.


Complete working implementation

This block defines an eight-step token scale, wires a card to a sizing container, and lays the cards out in a grid whose gap is itself a fluid token. Every bound is rem; every slope is cqi.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root {
    /* Fluid spacing tokens. Each is clamp(rem floor, rem base + cqi slope, rem ceiling).
       The cqi term means: grow with the component's own width. */
    --space-3xs: clamp(0.25rem, 0.2rem + 0.4cqi, 0.5rem);
    --space-2xs: clamp(0.5rem,  0.4rem + 0.6cqi, 0.75rem);
    --space-xs:  clamp(0.75rem, 0.6rem + 0.8cqi, 1rem);
    --space-s:   clamp(1rem,    0.8rem + 1cqi,   1.5rem);
    --space-m:   clamp(1.5rem,  1.1rem + 1.6cqi, 2.25rem);
    --space-l:   clamp(2rem,    1.4rem + 2.4cqi, 3.5rem);
    --space-xl:  clamp(3rem,    2rem + 3.6cqi,   5rem);
    --space-2xl: clamp(4rem,    2.6rem + 5cqi,   7rem);
  }

  body { margin: 0; font-family: system-ui, sans-serif; }

  /* The grid region establishes a container so its children's cqi resolves
     against this region's width, and uses a fluid token for the gap. */
  .grid {
    container-type: inline-size;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-m);
    padding: var(--space-l);
  }

  /* Each card is also its own container, so its internal padding/gaps
     scale to the card width, not the grid width. */
  .card {
    container-type: inline-size;
    display: grid;
    gap: var(--space-s);
    padding: var(--space-m);
    border: 1px solid #d0d0d0;
    border-radius: var(--space-xs);
  }

  .card h3 { margin: 0; }
  .card p  { margin: 0; line-height: 1.5; }

  /* A pill uses the smallest tokens; it spaces itself by its own width */
  .tag {
    container-type: inline-size;
    display: inline-block;
    padding: var(--space-3xs) var(--space-xs);
    border-radius: 999px;
    background: #7aa2ff33;
    font-size: 0.875rem;
  }
</style>
</head>
<body>
  <main class="grid">
    <article class="card">
      <span class="tag">Tokens</span>
      <h3>Proportional padding</h3>
      <p>This card's gaps grow with the card, not the page, because cqi resolves against the card.</p>
    </article>
    <article class="card">
      <span class="tag">Rhythm</span>
      <h3>Consistent scale</h3>
      <p>Every gap, margin, and pad references one of eight named tokens.</p>
    </article>
    <article class="card">
      <span class="tag">Zoom-safe</span>
      <h3>rem bounds</h3>
      <p>Floors and ceilings are in rem, so spacing respects browser zoom.</p>
    </article>
  </main>
</body>
</html>

Narrow the window and watch the cards reflow; each card's internal padding shrinks toward its rem floor while the grid gap shrinks independently, because they reference different containers.

Fluid spacing scale ramp Eight stacked bars, each labeled with a token name, growing in width from the smallest 3xs step to the largest 2xl step. spacing token ramp 3xs 2xs xs s m l xl 2xl

Key technique: cqi in the preferred term, rem in the bounds

The whole scale follows one shape per token: clamp(rem-floor, rem-base + N cqi, rem-ceiling). The cqi unit equals 1% of the container's inline size, so 1cqi on a 600px-wide container is 6px and on a 300px container is 3px — the slope automatically halves when the component is half as wide. That is exactly the proportional behavior a spacing scale wants. Keeping the floor and ceiling in rem means the token can never produce zero spacing on a tiny container nor a runaway gap on a huge one, and both bounds still answer to zoom. Adjust the single cqi coefficient per step to tune how aggressively that step responds to size.


Variation: tokens that also drive transition durations

Because each token is a plain custom property, the spacing scale can do double duty and feed motion timing, so larger components animate slightly slower and motion stays proportional to space. This is the same idea explored cross-area in Fluid Spacing Tokens Driving Transition Durations.

:root {
  /* Derive a duration from a spacing magnitude: more space, more travel,
     so allow a touch more time. Bounds keep it within sane motion limits. */
  --motion-s: clamp(120ms, 80ms + 1cqi, 220ms);
  --motion-m: clamp(180ms, 120ms + 1.6cqi, 320ms);
}

.card {
  transition: transform var(--motion-m) ease, box-shadow var(--motion-s) ease;
}

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
}

The reduced-motion guard is mandatory: proportional duration is still motion, and users who opt out must get none.

Browser support note

clamp() is supported in Chrome 79+, Edge 79+, Safari 13.1+, and Firefox 75+. Container query units cqi, cqw, and cqb require Chrome 105+, Edge 105+, Safari 16.0+, and Firefox 110+. Provide a static rem fallback before the container-unit rule, or wrap the cqi tokens in @supports (width: 1cqi), so engines without container units still get a sensible fixed scale.

FAQ

Why use cqi instead of vw for a spacing scale?cqi sizes against the nearest sizing container, so a component spaces itself by its own width. The same component then looks correct in a sidebar and a full-width region without per-context overrides, which vw cannot do.

Should spacing tokens use rem floors like typography tokens? Yes. Express the clamp() min and max in rem so spacing respects browser zoom and never collapses to an unusably small gap when a user enlarges text.

How many steps should a fluid spacing scale have? Six to eight named steps covers most interfaces. A common ratio is roughly 1.5 between adjacent steps, which keeps the rhythm visible without producing dozens of near-identical tokens.

Can the same tokens drive animation timing? Yes. Because the tokens are plain custom properties, the same scale that sizes gaps can feed transition durations, keeping motion proportional to spacing across the design system.

Related articles

More pages in the same section.