Fluid Spacing Tokens That Drive Transition Durations
Motion that feels right on a compact button can feel slow and heavy on a wide hero card, even with the identical duration, because the larger element travels farther for the same animation. The usual fix is to hand-tune durations per component, which scatters magic numbers and drifts out of sync the moment spacing changes. This guide, part of the CSS Custom Properties Architecture section, takes a different route: derive both spacing and transition durations from one shared set of fluid tokens, so when an element grows with the viewport or its container, its motion scales in proportion automatically. It deliberately reaches across into the responsive-sizing work in Mastering Container Queries & Responsive Layouts, because the tokens that size your layout are the same ones that should pace your motion.
The narrow scenario: a design system where padding, gaps, and travel distances scale fluidly, and you want the animation timing to track that scale instead of being a separate, manually maintained set of constants.
Why couple duration to the spacing scale
Perceived speed is not about absolute milliseconds; it is about velocity relative to size. A thumb that slides 24px in 200ms reads as snappy. A panel that slides 240px in the same 200ms reads as a blur, while at a comfortable velocity it would want closer to 360ms. If durations are hard-coded independently of size, every responsive breakpoint and every container-driven layout change silently desynchronises motion from geometry. By sourcing duration from the same fluid tokens that compute spacing — the scale built with techniques from fluid typography with clamp() and the space-scale approach in fluid space scale with clamp() — the relationship holds at every size with no per-component tuning.
The approach is pure CSS and stays declarative, which is its main advantage over a JavaScript layer that measures elements and sets durations at runtime: there is nothing to recompute on resize, nothing to debounce, and no main-thread cost. The tradeoff is arithmetic discipline — you must convert lengths into unitless ratios before multiplying them into a time, and you must keep the whole thing collapsible for reduced motion preferences. When proportional motion would be a distraction rather than a help (dense data tables, rapidly repeating toggles), keep durations flat instead.
One token, two outputs
The diagram shows the fan-out: a single fluid scale factor feeds both the spacing applied to an element and the duration of its transition, keeping the two locked together as the factor changes with viewport or container size.
A complete working implementation: a container-aware card
The card sizes its padding and lift distance from a fluid scale, then derives its hover duration from the same scale so a card rendered in a wide column animates over a longer, proportionate time than the same component in a narrow sidebar. The card queries its own container, so the scale tracks the container width via cqi.
<div class="grid">
<div class="card-wrap">
<article class="card"><h3>Proportional motion</h3><p>Resize me.</p></article>
</div>
</div>
.card-wrap {
container-type: inline-size; /* establishes the query container */
}
.card {
/* --scale: a unitless ratio from ~1 (narrow) to ~2 (wide).
cqi is 1% of the container's inline size; we map a width band
into a ratio with clamp() so it never runs away. */
--scale: clamp(1, 0.6 + 2cqi / 100, 2);
/* Spacing derived from the scale */
--pad: calc(0.75rem * var(--scale));
--lift: calc(8px * var(--scale)); /* how far the card rises on hover */
/* Duration derived from the SAME scale, converted to ms.
Base 140ms, stretched proportionally to the scale factor. */
--dur: calc(140ms * var(--scale));
padding: var(--pad);
border: 1px solid currentColor;
border-radius: 12px;
transform: translateY(0);
transition:
transform var(--dur) cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow var(--dur) ease;
}
.card:hover,
.card:focus-within {
transform: translateY(calc(-1 * var(--lift)));
box-shadow: 0 10px 28px rgb(0 0 0 / 0.18);
}
/* One override disables proportional motion everywhere */
@media (prefers-reduced-motion: reduce) {
.card { --dur: 0.01ms; }
}
.grid { display: grid; gap: 1rem; grid-template-columns: 1fr; }
Because --scale is computed once from cqi and then feeds --pad, --lift, and --dur, widening the container increases the padding, the lift distance, and the duration together. The card moves farther but over a longer time, so it keeps the same felt velocity at every container width.
/* DEMO: two columns so the same component renders at two scales */
@container (min-width: 400px) {
.grid { grid-template-columns: 1fr 1fr; }
}
The key technique: convert length to a ratio before timing it
The one move that makes this work is keeping the scale unitless. A duration must be a <time>, and you cannot multiply a length (cqi resolves to pixels) directly by a time. So the token --scale deliberately produces a plain number — clamp(1, 0.6 + 2cqi / 100, 2) divides the container unit down into a bounded ratio. That ratio is then safe to multiply into anything: a rem for padding, a px for travel, or a ms for duration via calc(140ms * var(--scale)). The shared ratio is the linchpin; spacing and timing are just two calc() expressions reading it. This is the same discipline used for animating custom properties with @property — keep the raw value typed and unit-clean, then convert at the point of use.
Variation: viewport-driven scale for full-bleed sections
Where the element spans the page rather than a container, swap cqi for vi (or vw) so the scale tracks the viewport instead. The rest of the arithmetic is unchanged, which is the point — only the source unit differs.
.hero {
--scale: clamp(1, 0.5 + 1.2vi / 100, 2.4);
--pad: calc(1rem * var(--scale));
--dur: calc(180ms * var(--scale));
padding-block: var(--pad);
transition: opacity var(--dur) ease, transform var(--dur) ease;
}
@media (prefers-reduced-motion: reduce) { .hero { --dur: 0.01ms; } }
A small phone gets snappy, restrained motion; a wide desktop hero gets a longer, more cinematic glide — from one formula.
Browser support note
The whole pattern rests on three well-supported features. calc() and custom properties are universal across evergreen browsers. clamp() is supported in Chrome and Edge 79+, Safari 13.1+, and Firefox 75+. Container query units (cqi, cqb) ship alongside size container queries in Chrome and Edge 105+, Safari 16+, and Firefox 110+, so the container-aware variant requires those versions; the viewport-unit variation works in any browser that has clamp(). Because the duration tokens degrade to ordinary fixed times if a unit is unsupported, the fallback is simply non-proportional but otherwise functional motion.
FAQ
Why should transition durations scale with layout size? Perceived speed is velocity relative to size: a larger element travels farther for the same animation, so a fixed duration that feels snappy on a small control reads as sluggish on a big card. Deriving duration from the same fluid tokens that size the element keeps the apparent speed consistent at every breakpoint.
Can I use cqi inside a custom property that feeds a duration?
Yes. Compute a unitless ratio from cqi or clamp() in one token, then multiply it by a base time with calc(). The custom property carries the raw ratio and the final calc() converts it into a valid <time> value such as calc(140ms * var(--scale)).
How do I keep proportional motion accessible?
Wrap the duration tokens in a prefers-reduced-motion: reduce query and collapse them to a near-zero value. Because every transition reads the shared token, that single override disables proportional motion across the whole system at once.
Do container query units work inside calc() for durations?
Container query units such as cqi resolve to lengths, so you must convert them to a unitless ratio first — for example by dividing through a reference length inside clamp() — before multiplying by a base duration. Support matches container query units: Chrome and Edge 105+, Safari 16+, and Firefox 110+.
Related
- CSS Custom Properties Architecture — the parent guide on token structure and theming.
- Animating custom properties with @property — make derived tokens themselves animatable.
- Fluid Typography with clamp() — the cross-area source of the fluid scaling math.
- Container query units (cqi, cqb) explained — the container-relative units that drive the scale.
- CSS Transition Timing Functions — pair proportional durations with the right easing.
Related articles
More pages in the same section.