Spring, Mass, and Motion
science behind animations that feel less like code, and more like intuition
Physics was always my first love in school - there was something magical about how it could explain everything around us – from why leaves fall to why the sky is blue.
As a developer who's been extensively using Framer Motion for the past few months, I've fallen in love with its spring animations. It's a beautiful marriage of my two passions: physics and programming.
While implementing these animations across various UI components in my projects, I noticed that many developers, including myself initially, weren't fully aware of what parameters like mass
, stiffness
, and damping
actually meant.
Thanks to my interest in physics, I can help demystify these concepts and show how the principles I learned back in school create these smooth, natural-looking animations.
I'm here for balance.
Hooke's Law: The Science of Springs
At its core, a spring animation follows the physics of what scientists call a Harmonic Oscillator. Don't let this term intimidate you – it's simpler than it sounds!
Back in school, we defined a Harmonic Oscillator like this:
A Harmonic Oscillator is a system that oscillates with a sinusoidal motion. In the context of a spring, this means that the spring will oscillate back and forth around its equilibrium position.
The motion of a spring can be described by the following equation:
This is the basic principle of a spring animation.
But wait, there’s more. Do you remember the three laws of motion?
I'm here for balance.
Newton's 2nd law of motion states that
The acceleration of an object is directly proportional to the net force acting on it and inversely proportional to its mass.
By combining these two formulas, we get:
Rearranging the equation, we get the acceleration:
i.e.
acceleration () =
This means the spring's acceleration depends on its stiffness
, displacement
, and mass
. From acceleration, we can figure out:
- Velocity (): How fast the object is moving.
- Position (): Where the object is at any point in time.
Wondering how? See if you can think of an answer, before scrolling further...
I'm here for balance.
Kinematics!
While Newton's 2nd law gives us the cause of acceleration (force), kinematic equations like tell us how velocity and position change as a result.
and similarly,
In both of the above equations, is the time interval between frames, which in the world of frontend animations is tied to the frame rate. For Framer Motion, this is typically 60 frames per second,
so or roughly 0.01666 seconds.
I'm here for balance.
Let's see how these physics equations translate into code.
Here's a function that calculates object positions over time based on spring motion:
1const calculateSpringMotion = (stiffness, mass, duration) => {
2
3 /* Length of the spring, let's assume it's 1 for simplicity */
4 const SPRING_LENGTH = 1;
5
6 const FRAME_RATE = 1 / 60; // i.e. 60 FPS
7
8 /* The total number of frames we want to generate */
9 const TOTAL_FRAMES = duration * FRAME_RATE;
10
11 let x = 5; // Initial position of the object
12 let v = 0; // Initial velocity is 0
13
14 /* Spring constant */
15 const k = -stiffness; // Negative as spring force opposes displacement
16
17 /* Initiate the array of position*/
18 const positions = [];
19
20 /* Calculate position for each frame */
21 for (let i = 0; i < TOTAL_FRAMES; i++) {
22 const force = k * (x - SPRING_LENGTH);
23 const a = force / mass;
24
25 v += a * FRAME_RATE;
26 x += v * FRAME_RATE;
27
28 positions.push(x);
29 }
30
31 return positions;
32
33};
I'm here for balance.
To make this interactive, I’ve created a playground below where you can experiment with the spring animation parameters. Adjust the mass and stiffness, to observe how the animation changes in real-time : )
Spring Motion Simulation
Want to build this?
Just pass these props to a motion.div — that's it!
1<motion.div
2 className="w-16 h-16 bg-indigo-600 rounded-full shadow-md"
3 initial={{ x: "-200%" }}
4 animate={{ x: "0%" }}
5 transition={{ type: "spring", stiffness, mass}}
6 />
I'm here for balance.
One of the things you might’ve noticed is that the animation never stops.
This actually, makes perfect sense from a physics perspective. In real life, In the real world, springs don’t just stop. They keep moving,subtly, endlessly until something slows them down or i.e. the energy dissipates.
However, in real life, the energy dissipates due to friction. In Framer Motion, we don’t have friction, so the animation never stops.
In order to simulate friction,
we need to add damping
to the spring.
Damping is a force that opposes the motion of an object. In the context of a spring, it means that the spring will lose energy over time and eventually stop oscillating.
The formula for damping is:
Taking this into account, our total force equation becomes:
This means that the acceleration equation becomes:
I'm here for balance.
Let’s make these changes in our code:
1const calculateSpringMotion = (stiffness, mass, damping, duration) => {
2
3 /* Length of the spring, let's assume it's 1 for simplicity */
4 const SPRING_LENGTH = 1;
5
6 const FRAME_RATE = 1 / 60; // i.e. 60 FPS
7
8 /* The total number of frames we want to generate */
9 const TOTAL_FRAMES = duration * FRAME_RATE;
10
11 let x = 5; // Initial position of the object
12 let v = 0; // Initial velocity is 0
13
14 /* Spring constant */
15 const k = -stiffness; // Negative as spring force opposes displacement
16
17 /* Damping Constant */
18 const d = -damping;
19
20 /* Initiate the array of position*/
21 const positions = [];
22
23 /* Calculate position for each frame */
24 for (let i = 0; i < TOTAL_FRAMES; i++) {
25
26 /* The one we calculated earlier was just the spring force */
27 const forceSpring = k * (x - SPRING_LENGTH);
28 const forceDamping = d * v;
29
30 const a = (forceDamping + forceSpring) / mass;
31
32 v += a * FRAME_RATE;
33 x += v * FRAME_RATE;
34
35 positions.push(x);
36 }
37
38 return positions;
39
40};
I'm here for balance.
Let's add another control damping
to our playground and update the calculateSpringMotion
function.
Spring Motion Simulation (with damping force)
The secret sauce?
Just add another damping
parameter to the transition prop in motion.div
1<motion.div
2 className="w-16 h-16 bg-indigo-600 rounded-full shadow-md"
3 initial={{ x: "-200%" }}
4 animate={{ x: "0%" }}
5 transition={{ type: "spring", stiffness, mass, damping}}
6 />
I'm here for balance.
As you can see, the spring animation gradually comes to a halt as damping dissipates energy from the system.
This is reflected in the chart, where the motion converges toward a final “resting position.”
By increasing the damping slider to a higher value, you’ll notice that the object reaches this “resting position” much faster compared to when the damping value is lower.
I'm here for balance.
In Motion (formerly framer-motion), these physics principles are abstracted away, but understanding them helps in creating better animations.
Here’s an example of different states in a dynamic island component (credits to Emil Kowalski) just to give you a sense of what you can do with these physics principles :)
Dynamic Island spring animation example
import { useMemo, useState } from "react"; import { motion } from "motion/react"; import { Ring } from "./ring"; import { Timer } from "./timer"; export default function DynamicIsland() { // State const [view, setView] = useState("idle"); const content = useMemo(() => { switch (view) { case "ring": return <Ring />; case "timer": return <Timer />; case "idle": return <div className="h-7" />; } }, [view]); return ( <div className="h-screen flex flex-col items-center justify-center p-4"> <div className="flex h-[100px] justify-center"> <motion.div layout style={{ borderRadius: 9999 }} className="h-fit min-w-[100px] overflow-hidden bg-black" > {content} </motion.div> </div> <div className="flex justify-center gap-4"> <button type="button" className="rounded-full w-16 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" onClick={() => setView("idle")} > Idle </button> <button type="button" className="rounded-full w-16 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" onClick={() => setView("ring")} > Ring </button> <button type="button" className="rounded-full w-16 h-10 bg-white px-2.5 py-1.5 text-sm font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" onClick={() => setView("timer")} > Timer </button> </div> </div> ); }
I'm here for balance.
The default values in Motion (formely framer-motion) stiffness: 100
, damping: 10
, mass: 1
work well for most cases, but now that you understand what each parameter does, you can fine-tune them to achieve exactly the animation feel you’re looking for ✨
I hope this helps you understand the physics behind spring animations and how you can use it to create better animations.
I'm here for balance.
P.S. So it turns out that, the same laws that made leaves fall in school,
now help buttons bounce, cards glide, and interfaces breathe :)