In the past few months, I've become a big fan of Framer Motion. After looking at how I could use it to animate my styled-components, I've been tinkering around spring animations and rebuilt almost all the transitions and animations of components in several UI projects. While showcasing the result to some fellow developers I got some questions around the meaning of some of the terms and options used to set up a spring animation like mass, stiffness, and damping. Most of them were setting them without really knowing how they were influencing the resulting animation. Lucky for them, I used to study maths and physics in college and was able to bring light on the physics behind this type of animation.
This article aims to explain how a spring animation in a library like Framer Motion works, the laws of physics that are behind it, and the relation between the different options you can set for a spring animation.
Hooke's Law
First of all, a spring animation has this name because the animation itself follows the physics of a spring or what we also call a Harmonic Oscillator. This term and the math surrounding it might seem very scary and complicated but bare with me, I'll break down everything as simply as possible. When I was in college, we defined a Harmonic Oscillator as follows:
a system that experiences a force (F) proportional to a displacement x when displaced from its equilibrium.
The formula for such force is called Hooke's Law and it is defined as follows:
1F = -k*x
where k
is a positive constant called stiffness which we can also write as:
What that means is that:
- if we pull the spring (i.e. x > 0 ) to a certain distance away from its equilibrium, it will start to move
- if we don't pull it, it won't move (i.e. x = 0)
However, maybe you might have heard at school or on one of the many science-focused Youtube channels on that force is the object's mass times its acceleration, which translates to the following formula:
1F = m*a
where m
is the mass and a
is the acceleration.
Thus given this formula and the formula above, we can deduct that:
1m*a = -k*x
which is equivalent to
1a = -k *x / m
We now have an equation from which we define the acceleration based on the displacement of our spring and the mass of the object attached to that spring. From the acceleration we can deduct the following:
- the velocity of the object at any given time
- the position of the object at any given time
To get the velocity of the object, you need to add the acceleration rate to the previously recorded velocity, which can translate to the following equation:
1v2 = v1 + a*t
Finally, we can get the position as it follows a similar principle: the position of the object is equal to the previously recorded position to which we add the velocity:
1p2 = p1 + v*t
For the time interval, as frontend developers, we might know it better as a frame rate or "frames per second". Considering the smoothness of Framer Motion's animations we can assume that its spring animations runs at 60 frames per second thus a time interval that is constant and equal to 1/60
or 0.01666
.
Translating the maths to Javascript
Now that we've done the math, you can see that by knowing the mass of the object, the stiffness and the displacement of our spring, we can know the position of the object attached to that spring at any given time, i.e at any given frame. We can translate all the equations above in Javascript, and for a given displacement calculate all the positions of an object for 600 frames, i.e. 10 seconds:
Function that returns the positions of an object following the motion of a spring
1const loop = (stiffness, mass) => {2/* Spring Length, set to 1 for simplicity */3let springLength = 1;45/* Object position and velocity. */6let x = 2;7let v = 0;89/* Spring stiffness, in kg / s^2 */10let k = -stiffness;1112/* Framerate: we want 60 fps hence the framerate here is at 1/60 */13let frameRate = 1 / 60;1415/* Initiate the array of position and the current framerate i to 0 */16let positions = [];17let i = 0;1819/* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/20while (i < 600) {21let Fspring = k * (x - springLength);2223let a = Fspring / mass;24v += a * frameRate;25x += v * frameRate;2627i++;2829positions.push({30position: x,31frame: i,32});33}3435/**36* positions is an array of number where each number37* represents the position of the object in a spring38* motion at a specific frame39*40* We use this array to plot all the position of the41* object for 10 seconds.42*/43return positions;44};
I built this small playground below with a graph representation of the positions that are returned by the function above a component animated by Framer Motion that has the same mass and stiffness. You can tune the mass and the stiffness with the range inputs above the graph and observe how each variable influences the animated component and the graph of positions.
Taking damping into account
While observing the visualization above, you might have wondered why is the spring animation never-ending as opposed to the ones you might have tried yourself with Framer Motion. That is because the math formulas we used to generate the position of the object were not taking into account friction and heat. If we want to obtain a spring animation that feels natural, we should see the movement of the object slowing down as time passes to eventually stop moving. That is where the damping comes into the picture. You might have seen this term when looking at the documentation of Framer Motion and wondered what it meant or does to the resulting spring animation, here's how we will define it:
Damping is the force that slows down and eventually stops an oscillation by dissipating energy
Its formula is:
1Fd = -d * v
where d
is the damping ratio and v
the velocity
Taking damping into account will bring some changes to the acceleration formula we established in the first part. We know that
1F = m*a
However, F here is equal to the spring force and the damping force, instead of just the spring force, thus:
1Fs + Fd = m*a -> a = (Fs + Fd)/m
We can now add this new formula to the Javascript code we've showcased in the previous part (I highlighted the additions I've made to the code compared to the previous implementation):
Updated function that takes into account the damping ratio
1const loop = (stiffness, mass, damping) => {2/* Spring Length, set to 1 for simplicity */3let springLength = 1;45/* Object position and velocity. */6let x = 2;7let v = 0;89/* Spring stiffness, in kg / s^2 */10let k = -stiffness;1112/* Damping constant, in kg / s */13let d = -damping;1415/* Framerate: we want 60 fps hence the framerate here is at 1/60 */16let frameRate = 1 / 60;1718let positions = [];19let i = 0;2021/* We loop 600 times, i.e. for 600 frames which is equivalent to 10s*/22while (i < 600) {23let Fspring = k * (x - springLength);24let Fdamping = d * v;2526let a = (Fspring + Fdamping) / mass;27v += a * frameRate;28x += v * frameRate;2930i++;3132positions.push({33position: x,34frame: i,35});36}3738return positions;39};
And finally, represent the resulting position data in the playground:
As you can see we now have a spring animation that eventually stops due to damping dissipating the energy out of the system. The chart above showcases this by converging towards a final "resting position". By increasing the damping slider to a high value you can observe that the object subject to spring animation tends to converge to the "resting position" way faster than for a lower damping value.
A real-life example
By default, Framer Motion sets the stiffness of the spring animation to 100, the damping to 10, and the mass to 1 according to the docs. Below, I wrote an animated Button
component that is closer to a real-life example that you might want to implement in your UI projects. Now that you know what mass, stiffness and damping you can try to fine-tune your spring animation.
import { motion } from 'framer-motion'; import './scene.css'; const Example = () => { return ( <motion.button style={{ background: 'linear-gradient(180deg, #ff008c 0%, rgb(211, 9, 225) 100%)', color: 'white', height: '50px', width: '200px', borderRadius: '10px', border: 'none', boxShadow: 'none', outline: 'none', cursor: 'pointer', }} whileTap={{ scale: 1.3, borderRadius: '6px', }} transition={{ type: 'spring', stiffness: 100, damping: 10, mass: 1 }} > Click me! </motion.button> ); }; export default Example;
Want to learn more the math/physics behind animations you see on a day-to-day basis?
I dedicated an entire blog post about the math behind Cubic Béziers that contains slick visualizations that easily explain how the motion these complex formulas define is obtained!
Want to learn more about Framer Motion?
Checkout my blog post Guide to creating animations that spark joy with Framer Motion!
Liked this article? Share it with a friend on Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.
Have a wonderful day.
– Maxime
A deep dive into the inner workings of spring animations in Framer Motion.