// prototyping_motion · easing_and_duration_model

Motion Timing Is a Physics Model, Not a Preset

Most interfaces treat easing as a dropdown pick — ease, ease-in-out, maybe a spring if the framework happens to offer one — chosen by how it feels on whichever component someone was building that day. Duration tied to distance, curves chosen by directional role instead of vibe, and a reduced-motion path that's actually designed rather than defaulted to zero are the parts that decide whether a hundred-screen product reads as one coherent system or a pile of individually-tuned transitions.

ADVANCED · 15 MIN READ · PROTOTYPING & MOTION

§ 01

Why "Ease In Out" Isn't a Decision

Ask most teams why a transition takes 200ms with an ease-in-out curve and the honest answer is usually "it felt right" — tuned once, on one component, by whoever happened to be building it that week. That's not a criticism of taste; it's a description of what happens when timing has no model behind it. The same product ends up with a 150ms fade here, a 400ms slide there, and a spring somewhere else entirely, each locally reasonable and collectively incoherent.

Motion timing is actually two independently tunable variables stacked on top of each other, plus a third model that replaces both of them entirely in some contexts:

Duration — how long the change takes, which should be a function of how much is changing, not a constant picked once and reused everywhere.

Easing — the velocity curve over that duration: whether the motion is constant, front-loaded, back-loaded, or something more deliberate — chosen by what role the moving element is playing, not by which curve looks nicest in isolation.

Physics parameters — an alternative to authoring a duration at all, where stiffness, damping, and mass describe the object and the system solves for motion frame by frame. Section five covers why that's not just a stylistic choice.

Inconsistent timing is exactly as visible to a user as inconsistent spacing — it just gets audited far less, because nobody screenshots a transition and holds a ruler to it. The rest of this guide is the model that replaces "it felt right" with something a second designer, or a second engineer, can actually reproduce.

§ 02

Duration Is a Function of Distance

Nothing in the physical world moves the same duration regardless of how far it travels, and motion that ignores this reads as artificial even when a viewer can't say why. An element sliding 8px into place and a panel sliding the full width of the screen should not share a duration — the first at 400ms looks sluggish, and the second at 100ms looks like it teleported.

duration ≈ base + k · √(distance)

That formula is the honest version of the relationship — a square-root curve, not a straight line, because doubling the distance shouldn't double the duration; perceived speed matters more than literal velocity, and a square-root relationship keeps large moves from feeling sluggish. Very few production teams implement a literal continuous function, though — almost all of them approximate it with a small set of named duration bands, tied to how much of the screen an element covers or crosses:

/ distance_duration_bands
Band Typical trigger Duration
Micro Icon toggle, checkbox, small state flip 100–150ms
Small Button press, chip select, in-place move 150–250ms
Medium Card or panel expand, dropdown, tooltip 250–400ms
Large Modal, full-screen sheet, route change 400–600ms

The ceiling matters as much as the bands. Past roughly 600–700ms, additional duration stops reading as "a bigger move" and starts reading as lag — the user's attention has already moved on before the animation finishes, which defeats the point of animating at all. If a transition genuinely needs to be slower than that, the fix is usually to break it into stages rather than stretch one curve across the whole thing.

None of these numbers are a law — they're a workable starting point that shows up, with minor variation, across most mature motion systems. What actually matters is picking a small, named set of bands and using them everywhere, instead of a fresh number every time a new component needs a transition.

§ 03

Reading a Cubic-Bézier Curve

cubic-bezier(x1, y1, x2, y2) plots progress (the y-axis, 0 to 1) against elapsed time (the x-axis, 0 to 1). The start and end points are fixed at (0, 0) and (1, 1) by definition — a transition always starts at 0% progress and ends at 100%, so those two points are never part of the four numbers you actually write. The four numbers describe two control points that pull the curve's shape between those fixed ends.

The first control point, (x1, y1), governs how the motion leaves its starting position — a low y1 relative to x1 means progress barely moves at first, producing a slow, held start. The second, (x2, y2), governs how it arrives — a y2 already close to 1 well before x2 reaches 1 means progress front-loads and coasts into the finish. Every named easing keyword in CSS is just a preset pair of these:

A cubic-bezier curve, plotted A blueprint diagram plotting progress against time for the CSS default "ease" curve, with its two control points shown as handles pulling away from the straight diagonal a linear curve would trace. time (0→1) progress (0→1) 0 1 linear (for reference) P0 (0, 0) P1 control (0.25, 0.1) P2 control (0.25, 1) P3 (1, 1) cubic-bezier(0.25, 0.1, 0.25, 1) — CSS's default "ease"
FIG. 01 — P0 and P3 are always fixed at (0,0) and (1,1); only the two control points are ever configurable. This is the curve most transitions ship with by default, unnoticed.

The five CSS keywords resolve to fixed control-point pairs — worth memorizing, because ease is what a transition silently uses whenever transition-timing-function is left unset, not a neutral "no easing" default:

/ css_easing_keywords
Keyword cubic-bezier() Shape
linear (0, 0, 1, 1) Constant velocity, no easing at all
ease (0.25, 0.1, 0.25, 1) The silent default — slight start, long coast
ease-in (0.42, 0, 1, 1) Slow start, fast finish
ease-out (0, 0, 0.58, 1) Fast start, slow finish
ease-in-out (0.42, 0, 0.58, 1) Symmetric — slow, fast, slow

One more thing worth knowing: y1 and y2 aren't restricted to the 0–1 range the way x1 and x2 are — a control point with a y-value above 1 or below 0 is legal, and that's the mechanism behind overshoot and anticipation curves, where an element visibly swings past its resting value before settling back, or pulls back slightly before committing to a move.

§ 04

Standard, Decelerate, Accelerate

Most mature motion systems don't hand every component the same curve — they define curves by the directional role the moving element is playing, then pair each role with a shape that matches how that kind of motion actually reads:

Standard — for an element that's on screen for the whole transition, changing position or size without entering or leaving. A moderate ease in both directions keeps it readable without calling attention to either end.

Decelerate — for an element entering the screen. It starts already moving at speed, as if it existed off-screen a moment ago, and slows into its resting place. That deceleration is what reads as "arriving," not just appearing.

Accelerate — for an element leaving the screen. It starts at rest and gains speed on the way out, because the user doesn't need to see exactly where it lands — only that it's decisively gone.

A well-known reference version of these three, stable across years of Material-style motion specs, illustrates the shapes well: standard as cubic-bezier(0.4, 0, 0.2, 1) ("fast out, slow in"), decelerate as cubic-bezier(0, 0, 0.2, 1), and accelerate as cubic-bezier(0.4, 0, 1, 1). The exact numbers matter far less than the pairing logic behind them.

Standard, decelerate, and accelerate curves compared A blueprint diagram plotting three easing curves on the same progress-versus-time axes: a standard curve eased at both ends, a decelerate curve that starts at full speed, and an accelerate curve that ends at full speed. time (0→1) progress standard — stays on screen decelerate — entering accelerate — exiting
FIG. 02 — All three share the same start and end points; only the control points, and the role each curve is assigned to, differ. Swapping decelerate and accelerate between an entrance and an exit is the pitfall in section nine.

§ 05

Spring Physics: A Different Model

Every curve in sections three and four shares one trait: it's a fixed-duration curve, baked in advance. Spring physics replaces that entirely. Instead of authoring a duration and a shape, you set physical parameters — stiffness (how strongly the spring pulls toward its target), damping (how much that pull is resisted, converting energy to settle the motion), and mass (how much inertia the moving object has) — and the system solves a damped harmonic oscillator each frame. Duration becomes an output of the simulation, not an input to it.

F = −k·x − c·v

Where k is stiffness, x is displacement from the target, c is the damping coefficient, and v is current velocity. How that force resolves over time falls into three regimes worth knowing by name:

Underdamped — the object overshoots the target and oscillates before settling. Visually bouncy; useful for playful or attention-grabbing moments, wrong for anything that needs to feel controlled.

Critically damped — the object returns to rest in the minimum possible time with no overshoot at all. This is the default most interfaces actually want: fast, natural, no bounce.

Overdamped — the object approaches the target slowly with no oscillation, arriving well after a critically damped spring would. Rarely what a UI needs; mostly useful as a deliberately heavy, weighted feel.

The practical reason springs matter isn't the physics vocabulary — it's interruptibility. A bezier curve interrupted mid-flight has no defined behavior for "what happens now"; someone has to hand-author a transition out of wherever the animation happens to be paused, which is expensive to do well and usually skipped. A spring is defined entirely by its current position and velocity, so re-targeting it mid-motion — because the user dragged, tapped again, or reversed a gesture — is the normal case, not a special one. That's why gesture-driven interactions like drag-to-dismiss or swipe-back lean on springs rather than pre-baked curves: the interface has to stay honest about velocity the instant a finger changes direction.

The trade-off is authoring workflow. A duration in milliseconds is trivial to write into a design spec; a stiffness-and-damping pair is not something most handoff tools display, and two engineers implementing "the same spring" on different platforms can easily land on visibly different motion unless the actual numbers — not just a description like "snappy" — are written down and shared.

§ 06

Choreography Without Chaos

When several elements enter or leave together, animating all of them at once reads as a flash rather than a sequence — the eye can't track a dozen things changing in perfect unison. A small stagger, offsetting each item's start time from the last, lets a group still read as one coordinated motion while remaining perceptible as a sequence rather than a blur.

itemDelay = baseDelay + (index · staggerStep)

A stagger step of roughly 20–50ms between items is enough to read as sequential without dragging the whole group's entrance out. That step needs a ceiling, though — applied naively to a 30-row list, a 40ms step alone adds 1.2 seconds to the last row's entrance, which stops feeling like choreography and starts feeling like the list is loading slowly. Capping the total spread, or switching to a flat short delay past some item count, keeps the effect proportional.

Direction is a deliberate choice, not a default. Entrances typically stagger forward — first item first — so attention follows the group in reading order. Exits often reverse that order, last item first, so the eye's attention retraces its own path back out instead of jumping around. Getting this backwards is subtle enough that it rarely gets flagged in review, but it's noticeably why some staggered exits feel slightly wrong without an obvious cause.

One more choreography rule worth stating explicitly: a parent container's transform and a child's opacity fade shouldn't run on unrelated curves with unrelated durations. Two different eased motions compounding on nested elements produces a combined curve that's neither one — not broken, exactly, but perceptibly "off" in a way that's hard to diagnose because neither individual animation, viewed alone, looks wrong.

§ 07

Motion Tokens and the DTCG Types

Motion values belong in the same token system as color and spacing, not hardcoded per-component — the same naming discipline applies. Duration tokens should be named by the distance band from section two (motion.duration.fast, .base, .slow), and easing tokens should be named by directional role from section four (motion.easing.standard, .decelerate, .accelerate) — never by shape, for the same reason a color token shouldn't be named after its hex value. A token called motion.easing.bouncy is a description wearing a role token's clothes; the day that curve gets retuned to something that isn't bouncy anymore, every consumer of the name is now wrong.

The W3C DTCG format — covered in more depth in the tokens guide linked above — defines two primitive types that cover motion directly: $type: "duration", a number plus a unit string like "300ms", and $type: "cubicBezier", an array of exactly four numbers. A composite "transition" type, bundling a duration, an easing reference, and an optional delay into one resolvable unit, is worth adopting even where it isn't formally standardized yet — it keeps a duration and its paired curve from drifting apart when one gets retuned without the other:

{
  "motion": {
    "duration": {
      "fast":  { "$value": "150ms", "$type": "duration" },
      "base":  { "$value": "250ms", "$type": "duration" },
      "slow":  { "$value": "450ms", "$type": "duration" }
    },
    "easing": {
      "standard":   { "$value": [0.4, 0, 0.2, 1], "$type": "cubicBezier" },
      "decelerate": { "$value": [0,   0, 0.2, 1], "$type": "cubicBezier" },
      "accelerate": { "$value": [0.4, 0, 1,   1], "$type": "cubicBezier" }
    },
    "transition": {
      "panel-enter": {
        "$type": "transition",
        "$value": {
          "duration": "{motion.duration.base}",
          "timingFunction": "{motion.easing.decelerate}",
          "delay": "0ms"
        }
      }
    }
  }
}

§ 08

Reduced Motion Isn't "Off"

prefers-reduced-motion is an OS-level media feature — the user sets it once, system-wide, and it exposes two values to a stylesheet: reduce and no-preference. For some users this isn't an aesthetic preference: large-scale motion — parallax, zoom, spin, sweeping transforms — can trigger real physical symptoms for people with vestibular disorders, not just mild annoyance. Treating the setting as optional polish rather than a genuine accessibility requirement gets the stakes backwards.

The common mistake is treating "respect reduced motion" as "set every duration to 0ms." A hard, instant cut can actually be more disorienting than a short, simple animation, because it removes the continuity cue that told the user where the new state came from — a menu that simply vanishes and reappears elsewhere reads as two unrelated events, not one continuous change. The better approach keeps the state change legible — a brief opacity crossfade, or a small position shift — while stripping out the large-scale transform, parallax, or zoom that could actually cause harm.

In practice, that means the media query should swap tokens, not delete the transition system:

@media (prefers-reduced-motion: reduce) {
  :root {
    --motion-duration-base: 80ms;
    --motion-duration-slow: 120ms;
    --motion-easing-decelerate: linear;
    --motion-easing-accelerate: linear;
  }
  .parallax-layer,
  .hero-zoom {
    transform: none !important;
  }
}

This keeps the reduced-motion path a designed, tested state — not an unstyled fallback nobody looked at until a user reported it. The same structural discipline that governs focus and keyboard behavior applies here: reduced motion is a state to design for deliberately, not a media query to bolt on at the end.

§ 09

Five Pitfalls That Look Like Polish

  • 01

    Picking the curve before the duration. Easing shapes how a duration feels, but it can't fix a duration that's wrong for the distance in section two. Set the duration band first, from what's actually moving, then apply the curve — reversing the order usually means re-tuning the curve later to compensate for a duration that was never right.

  • 02

    Shipping the silent ease default everywhere. Every transition: all 200ms; shortcut that omits a timing function inherits cubic-bezier(0.25, 0.1, 0.25, 1) from section three, regardless of whether that shape fits the motion's role. The pool of "unintentional" curves in a codebase grows every time this shortcut ships, and it compounds silently because nothing about it looks like a bug.

  • 03

    Animating layout properties instead of transform and opacity. Transitioning width, height, top, or left forces the browser to recalculate layout on every frame, which is where animation jank comes from on lower-power devices. transform and opacity can run entirely on the compositor, independent of layout — the same visual result, without the cost.

  • 04

    Swapping the decelerate and accelerate roles from section four. Using an entrance curve on an exiting element makes it read as if it hesitates before leaving; using an exit curve on something arriving makes the arrival feel abrupt. Both are subtle enough to survive review unnoticed while still registering, faintly, as something being slightly wrong.

  • 05

    Treating reduced motion as a launch-week afterthought. Bolting a prefers-reduced-motion block onto a finished motion system, under deadline pressure, is how reduced-motion paths ship broken or don't ship at all. Design the reduced state alongside the full one from the start, the way section eight describes, and it never becomes a separate line item to run out of time for.

§ 10

Quick Reference

/ motion_timing_reference
Concept Typical value Note
Micro / small duration 100–250ms Icon toggles, buttons, in-place moves
Medium / large duration 250–600ms Panels, modals, route-level transitions
Practical ceiling ~600–700ms Past this it reads as lag, not motion
CSS silent default cubic-bezier(.25,.1,.25,1) Applied automatically when unset
Entrance role decelerate Already moving; slows into place
Exit role accelerate Starts at rest; speeds up leaving
Critically damped spring c = 2√(k·m) Fastest return with no overshoot
Stagger step 20–50ms per item Cap total spread on long lists
prefers-reduced-motion reduce / no-preference OS-level, not a JS or app flag

None of the specific numbers in this guide are the point — a team will retune duration bands and curve shapes as the product's voice evolves, and that's expected. What's meant to outlast any single value is the model underneath: duration keyed to distance rather than picked per component, curves assigned by directional role rather than chosen for how they look in isolation, and a reduced-motion path treated as a real designed state rather than a compliance checkbox. Whatever specific milliseconds a team eventually settles on, keeping them behind that model is what makes a hundred-screen product's motion feel like one hand designed all of it, rather than whichever engineer happened to touch that particular transition last.

PROTOTYPING EASING SPRING PHYSICS MOTION TOKENS REDUCED MOTION