Scaling Duration by Distance and Size

Part of Timing Functions & Easing Curves in Core CSS Animation Fundamentals.

The problem

A kanban board animates cards to their new positions with a fixed 300ms transition. Moving a card one slot down looks slow and floaty. Moving a card from the first column to the last on a wide screen looks like it teleports, because it crosses 1,200 pixels in the same 300ms. A bottom sheet and a small tooltip share the same entrance duration, and the sheet feels rushed while the tooltip feels lazy.

In the physical world heavier objects and longer journeys take longer. Motion that ignores distance and size feels uniform in time and inconsistent in speed.

Root cause analysis: speed is distance over time

A fixed duration fixes time, so speed varies with distance. A 40px move at 300ms averages 133px per second; a 1,200px move averages 4,000px per second. Human perception is closer to judging speed, so the short move feels slow and the long one feels abrupt.

A fixed speed has the opposite problem: long moves become very long. A 1,200px move at 400px per second takes three seconds, which is far too slow for an interface.

Sub-linear scaling sits between the two. Growing duration with the square root of distance keeps short moves quick and makes long moves take longer, but not proportionally longer. Material-style guidance and many motion systems use variations of this idea: duration increases with distance, with diminishing increments.

Size follows the same logic. Larger elements displace more of the screen, and a large surface covering the viewport in 150ms feels violent, while a 16px badge taking 400ms feels sluggish. Duration tiers by element size — small, medium, large, full-screen — are the static version of the same principle.

Clamping is essential. Whatever the formula, set a minimum so tiny moves remain perceptible and a maximum so nothing keeps the user waiting. A range of roughly 150ms to 450ms covers most interface motion.

Duration against distance for three strategiesNormalised: 0 to 1200 px on x, 0 to 3 s on y. Square root stays within the clamp.Duration against distance for three strategies1.000.750.500.250.000.000.250.500.751.00elapsed time (fraction of duration)progressfixed 300 msconstant speed 400 px/ssqrt, clamped 150 to 450 ms
Normalised: 0 to 1200 px on x, 0 to 3 s on y. Square root stays within the clamp.

Step-by-step resolution

Distance-aware transitionsMeasure once, compute in CSS, clamp always.Distance-aware transitions1Before moving the element, measure its old and new positions once.Distance in pixels2Write the distance to a custom property on the element.CSS can read it3Compute duration with calc and sqrt from a base and a rate.Sub-linear growth4Wrap the result in clamp() with a minimum and maximum.Never too fast or too slow5Use size tiers for entrances where distance is implicit.Tooltips quick, sheets slower6Replace the formula with a short constant under reduced motion.
Measure once, compute in CSS, clamp always.

Production code pattern

:root {
  --dur-min: 150ms;
  --dur-max: 450ms;
  --dur-rate: 8ms;              /* ms per sqrt(px) */
}

.card {
  /* --distance is unitless pixels, written by script before the move. */
  --dur: clamp(
    var(--dur-min),
    calc(var(--dur-min) + sqrt(var(--distance, 0)) * var(--dur-rate)),
    var(--dur-max)
  );
  transition: translate var(--dur) cubic-bezier(0.4, 0, 0.2, 1);
}

/* Size tiers for entrances where distance is implied by size. */
.tooltip      { --enter: 120ms; }
.menu         { --enter: 200ms; }
.bottom-sheet { --enter: 320ms; }
.fullscreen   { --enter: 400ms; }
:is(.tooltip, .menu, .bottom-sheet, .fullscreen)[data-open] {
  transition: opacity var(--enter) ease-out, translate var(--enter) cubic-bezier(0.2, 0.8, 0.2, 1);
}

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
  :is(.tooltip, .menu, .bottom-sheet, .fullscreen)[data-open] {
    transition: opacity 120ms ease-out;
  }
}
// FLIP-style move: measure, write distance, then let the transition run.
function moveCard(card, newParent) {
  const before = card.getBoundingClientRect();
  newParent.append(card);
  const after = card.getBoundingClientRect();
  const dx = before.left - after.left;
  const dy = before.top - after.top;
  card.style.setProperty('--distance', String(Math.hypot(dx, dy)));
  card.style.transition = 'none';
  card.style.translate = `${dx}px ${dy}px`;          // appear at the old spot
  requestAnimationFrame(() => {
    card.style.transition = '';
    card.style.translate = '';                        // transition to the new spot
  });
}

Rendering Impact: composite for the move. The duration is resolved when the transition starts from a static custom property; per frame, only translate changes. The two layout reads happen once per move, before any writes.

sqrt() is one of the newer CSS math functions. Where it is unsupported the whole --dur computation is invalid and the transition falls back to its initial duration of 0 — an instant move. Guard it with @supports (width: calc(sqrt(4) * 1px)) and a fixed-duration default, or compute the duration in script and write it directly.

Durations for three card movesFixed 300 ms against the clamped square-root formula above.Durations for three card moves40 px, fixed300 ms40 px, scaled201 ms400 px, fixed300 ms400 px, scaled310 ms1200 px, fixed300 ms1200 px, scaled427 ms
Fixed 300 ms against the clamped square-root formula above.

Verification checklist

Constraints and trade-offs

  • Distance-scaled durations make simultaneous moves finish at different times, which can look disorganised in grids; consider using the longest duration for a group.
  • The rate constant needs tuning per product; start around 6 to 10ms per square-root pixel.
  • High-density and large screens produce longer distances in CSS pixels for the same visual movement.
  • Measuring positions forces layout; batch measurements for many elements.
  • Designers may prefer tiered durations over formulas for predictability.

Frequently asked questions

Should longer animations always take longer?

Somewhat longer, not proportionally. Growing duration with the square root of distance keeps long moves from feeling abrupt without making them slow.

What minimum and maximum durations should I use?

Around 150ms minimum so motion is perceptible, and 400 to 500ms maximum for interface motion so users are not kept waiting.

Can CSS calculate duration from distance on its own?

CSS cannot measure distance, but it can compute duration from a custom property set by script using calc(), sqrt() and clamp().

How do size tiers relate to distance scaling?

They are the static version of the same idea. When an element enters from off-screen, its size stands in for the distance it travels.