// 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.
§ 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:
| 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:
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:
| 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.
§ 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
easedefault everywhere. Everytransition: all 200ms;shortcut that omits a timing function inheritscubic-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, orleftforces the browser to recalculate layout on every frame, which is where animation jank comes from on lower-power devices.transformandopacitycan 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-motionblock 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
| 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.