Engine 2026.2.0
Method
Motion scores are only useful if you can see how they were produced. Everything below is the actual algorithm, in the order it runs.
1. Springs are integrated, not approximated
A spring with stiffness k, damping c and mass m obeys x'' = (−k·x − c·v) / m. The engine starts from rest at the requested displacement and advances with a fixed-step semi-implicit (symplectic) Euler solver at 240 Hz — well above any display refresh rate.
const DT = 1 / 240; // fixed step
for (let i = 0; i <= steps; i++) {
const accel = (-k * x - c * v) / m;
const vNext = v + accel * DT; // velocity from force
x = x + vNext * DT; // position from new velocity
v = vNext;
}The step size is fixed rather than adaptive, and that is deliberate: a fixed step is what makes the result deterministic. The same parameters produce a bit-identical trace on any machine, in any browser, forever. Integration stops after six seconds, after which a motion that has not settled is reported as non-convergent rather than given a made-up number.
2. Rest needs a velocity condition
A common shortcut is to declare a spring settled when it passes within a small distance of the target. That is wrong: an under-damped spring passes through the target at high speed several times before it actually stops. Treating a fast pass as arrival reports zero overshoot for a violently bouncing motion.
Folio Motion requires both a position and a velocity condition. With ω = √(k/m) being the undamped natural frequency, a body at velocity v would coast roughly v/ω further, so rest is only declared when |v| ≤ threshold · ω as well as |x| ≤ threshold.
3. Beziers are solved like the browser solves them
A cubic bezier easing function is not evaluated as a polynomial in time. The x component is inverted first — Newton-Raphson with a bisection fallback, then the y component is sampled at the resulting parameter. This is the same approach CSS uses for cubic-bezier(), which means a curve that looks right here behaves identically in a stylesheet.
Control-point x values are clamped to [0, 1] because a timing function with an x outside that range is not monotonic and would travel backwards in time.
4. Five weighted factors, all measured
Each factor is a 0–100 goodness value derived from a measured quantity, multiplied by its weight. The score is the clamped sum, so the arithmetic is fully auditable: the contributions you see in the UI add up to the headline number.
Settle time
weight 30 / 100The first moment the value is within 0.5% of target AND slow enough that it would stay there. Requiring both conditions is what separates a spring that arrives from one that merely swings through at speed.
Overshoot
weight 25 / 100Peak position past the target, as a fraction of total travel. Measured from the integrated trace, not approximated from the damping ratio.
Onset latency
weight 20 / 100Time to cover the first 10% of travel. This is the number users actually feel as responsiveness, and it is independent of total duration.
Velocity profile
weight 15 / 100Peak velocity normalized by travel distance, penalized when the motion never converges. High normalized velocity reads as a snap rather than a glide.
Accessibility
weight 10 / 100Penalties for animating layout-triggering properties, for blur, for very long settle times, and for extreme overshoot — all of which can cause discomfort for people with vestibular disorders.
5. History is sealed, not trusted
Each spec owns an independent chain starting from a genesis value. Every event is sealed with SHA-384(prevSeal ‖ canonicalJson(event)), where the JSON is canonicalized by recursively sorting object keys. Replaying recomputes every seal and reports the first mismatch, so a silently edited record is detectable rather than merely improbable.
genesis = 000000000000000000000000…
Replay the chainLimits worth knowing
- This is not a substitute for testing with users. The numbers describe physical behaviour. Whether a motion is appropriate is a design judgement no score can make.
- Frame cost is modelled, not measured. The engine reasons about which properties force layout, but it does not run a real compositor. Actual jank depends on the page, the device and the frame budget.
- Cubic beziers are sampled, not solved analytically. The trace has finite resolution; settle time for a bezier is its declared duration.
- Reduced motion is your responsibility in the output. Every exported CSS includes a
prefers-reduced-motionblock, and the Framer Motion export returns a zero-duration transition when the hook is set. Wire it up.