Spring Animation in CSS

where design, programming, physics and calculus meet

Thai Pangsakulyanont
10 min readMar 30, 2016

When doing animations in CSS, most commonly, one would either use the default CSS timing functions provided by CSS, or go to cubic-bezier.com and create their own easing function, in order to customize the type of motion.

However it’s almost impossible to simulate a realistic elastic spring-like animation with these basic timing functions.

This article will show you how to use CSS animations to implement a realistic spring-like animation, based on physics and calculus!

(Mobile users: If you don’t see the animated example, please try this Codepen link)

By the way, I am not either a physicist or a mathematician. I learned only the first few elementary courses on these subjects (as they’re mandatory, and I got bad grades at it). Therefore, the words I use may not be the most appropriate ones. Or maybe some explanation might be entirely wrong. I’m just a web developer. Please feel free to send me corrections. :)

Why CSS animation?

With JavaScript, you have much more expressive power in describing how something animates. react-motion and Velocity.js are some popular examples. But there’s one problem: you have only one JavaScript thread that has access to the DOM.

For example, you may want to show an animated spinner while your application is downloading some data. Thousands of DOM nodes will be created as a result.

If you animate your spinner using JavaScript, the spinner will stop spinning while the expensive DOM operation is taking place.

However, if you use CSS animations, the spinner will keep spinning, even though your web page is totally unresponsive (at least in Chrome)!

The Basics of Tweening

Let’s say I want to animate a box from left: 100px to left: 200px.

This means, over time, I want less of 100px and more of 200px.

Let’s elaborate: As the animation progresses, I want the proportion of 100px to decrease (from 100% to 0%), and at the same time, I want the proportion of 200px to increase (from 0% to 100%).

We’ll refer to the proportion of the 200px as the progress of the animation, or more concisely, p. We can find the left position at any given p using this formula:

This image should explain why it works:

Let’s generalize it. If we want to tween a property from value A to B, during the motion at progress p, the value of the property is:

Some may also prefer to write it like this, which is also equivalent (but requires fewer multiplications):

This is called ‘lerp’ or ‘linear interpolation.’

But real world animations aren’t linear. For example, you may want the animation to start slowly, and ends quickly. That’s where an easing function comes into play, so that the progress and time doesn’t have to go linearly.

An easing function establishes the relation between time and animation’s progress.

There’s also an excellent tutorial about easing on mo · js website.

The Physics

Here is a spring with a block attached to it. Let’s say at equilibrium, the block would be at position x = 1:

Now, you push the block, so that its position becomes x = 0.

When you stop pushing it, the spring will cause the block to move back and forth, until the spring reaches equilibrium at x = 1.

This is like an easing function! It’s a function of time that starts at 0%, and ends at 100%. This spring is one of them!

Now, if you’ve taken an introductory physics course, you’d probably have learned some equations about springs.

First, there’s this spring force that pulls the block back into its equilibrium position. Here, X is the displacement of the block’s from its equilibrium position.

There’s also this damping force which slows down the motion. Without it, the spring will keep oscillating forever.

Therefore, this is the resulting force that is exerted on the block:

We also know from Newton’s 2nd law that:

Let’s assume, for simplicity, that the mass is m = 1. Therefore, we have:

Again, X represents the displacement of the block from its equilibrium position. This means if we are at position x = x and we want to go to x = 1, then we need to move by (x-1) unit to reach that position.

That’s the displacement, so we’re now left with this.

That’s our motion equation.

Now we’re done with the physics!

The Calculus

If you’ve learned some elementary calculus, you’d probably know that we can represent the position of something in motion with a function of time.

Take the derivative of the position, and you get the velocity.

Take the derivative of the velocity, and you get the acceleration.

Recall this motion equation from the previous section:

Substitute x, v, and a. Now you have this:

Now, this is a differential equation. The solutions to this equation would be the function f(t) that satisfies the above equation.

I cannot solve it using my elementary calculus knowledge, so I’ll cheat by asking Wolfram|Alpha to solve it for me.

But before we do that, let’s set a few constraints to make this less complicated.

Remember that before we let the block move, we pushed the block to the position x = 0. That’s the initial position, the position at time t = 0. Therefore, we have this:

We also know that before we let it go, the block doesn’t move. This means we also have this:

Unfortunately, as of writing, Wolfram|Alpha couldn’t solve this equation with k and c left as variables, so we need to put in some values.

I’m going to take the wobble preset from the react-motion library. It has a stiffness of k = 180, and the damping factor of c = 12.

Therefore, we end up with this set of equations:

And that’s what I asked Wolfram|Alpha:

f(0) = 0; f'(0) = 0; f''(t) = -180(f(t) - 1) - 12f'(t)

And Wolfram|Alpha gave me this solution:

Where did ½, 6, sin and cos come from? I don’t know. ¯\_(ツ)_/¯

But it looks legit, as sin and cos feels like an oscillating motion, just like a spring. I’ll plot it on a graph from t = 0 to t = 1 by sampling the function’s value by 0.01. Here’s how it looks like.

It looks elastic, like a spring!

The CSS animation

You cannot put such a complex equation into CSS, but you can generate some CSS to closely approximate this equation. CSS animation only lets you specify discrete keyframes, but our equation is a continuous function.

Just like how I plotted the graph above, but instead of plotting into a graph, I will plot it into CSS keyframes. For example, if the animation will last 1 second, the CSS will look like this:

@keyframes keyframe-name {
0% { /* css code for t=0.00s */ }
1% { /* css code for t=0.01s */ }
2% { /* css code for t=0.02s */ }
3% { /* css code for t=0.03s */ }
/* ... */
99% { /* css code for t=0.99s */ }
100% { /* css code for t=1.00s */ }
}

Obviously, generating this by hand is not practical. We’re going to use some CSS preprocessing language to generate this. I’ll use Stylus.

First, I’ll write a Stylus function based on what Wolfram|Alpha told me:

spring-wobbly(t)
return -0.5 * (2.71828 ** (-6 * t)) * (
-2 * (2.71828 ** (6 * t)) + sin(12 * t) + 2 * cos(12 * t))

Also recall the interpolation function equation we covered in the tweening basics section. I’ll write a function to interpolate between two values:

lerp(a, b, p)
return a + p * (b - a)

Next, I’ll generate the keyframes.

@keyframes move
for i in (0..100)
{i + '%'}
t = i / 100
p = spring-wobbly(t)
left: lerp(100px, 200px, p)

Stylus generated this CSS:

@keyframes move {
0% { left: 100px; }
1% { left: 100.86376425736435px; }
2% { left: 103.30887625352196px; }
3% { left: 107.11510629593647px; }
4% { left: 112.06407562318391px; }
5% { left: 117.94274565577675px; }
6% { left: 124.54642206432976px; }
7% { left: 131.68127828753256px; }
8% { left: 139.16640928859013px; }
9% { left: 146.83543411253555px; }
10% { left: 154.53767168481676px; }
11% { left: 162.13891722725492px; }
12% { left: 169.5218524015242px; }
13% { left: 176.586122452165px; }
14% { left: 183.24811775766403px; }
15% { left: 189.44049645431824px; }
16% { left: 195.11148520727033px; }
17% { left: 200.22399583479304px; }
18% { left: 204.7545926401348px; }
19% { left: 208.6923456959847px; }
20% { left: 212.03760160865943px; }
21% { left: 214.80070272750575px; }
22% { left: 217.0006815538447px; }
23% { left: 218.663955706991px; }
24% { left: 219.82304538616665px; }
25% { left: 220.51533264040694px; }
26% { left: 220.78187825980012px; }
27% { left: 220.66631056510982px; }
28% { left: 220.21379606281764px; }
29% { left: 219.47010051916436px; }
30% { left: 218.48074615498618px; }
31% { left: 217.29026816681414px; }
32% { left: 215.94157202720172px; }
33% { left: 214.47539120167033px; }
34% { left: 212.92984275093278px; }
35% { left: 211.34007772340152px; }
36% { left: 209.7380212102259px; }
37% { left: 208.15219633458372px; }
38% { left: 206.60762598024857px; }
39% { left: 205.12580475213764px; }
40% { left: 203.72473387120573px; }
41% { left: 202.41901137453345px; }
42% { left: 201.2199697208098px; }
43% { left: 200.13585305311918px; }
44% { left: 199.17202686633095px; }
45% { left: 198.33121263498484px; }
46% { left: 197.61374067964204px; }
47% { left: 197.01781499211256px; }
48% { left: 196.53978407975245px; }
49% { left: 196.17441264256894px; }
50% { left: 195.91514930968282px; }
51% { left: 195.75438645693993px; }
52% { left: 195.68370845882814px; }
53% { left: 195.69412560592633px; }
54% { left: 195.7762912646242px; }
55% { left: 195.92070055484277px; }
56% { left: 196.1178692094388px; }
57% { left: 196.35849187467915px; }
58% { left: 196.63357948554946px; }
59% { left: 196.93457574055066px; }
60% { left: 197.25345307939384px; }
61% { left: 197.582788848219px; }
62% { left: 197.91582261394115px; }
63% { left: 198.24649577847353px; }
64% { left: 198.56947483257156px; }
65% { left: 198.880159678671px; }
66% { left: 199.17467862706735px; }
67% { left: 199.44987158683784px; }
68% { left: 199.70326312039px; }
69% { left: 199.93302695538216px; }
70% { left: 200.13794350123476px; }
71% { left: 200.31735190782865px; }
72% { left: 200.47109806939648px; }
73% { left: 200.5994799360452px; }
74% { left: 200.70319134949088px; }
75% { left: 200.78326552237604px; }
76% { left: 200.84101916476513px; }
77% { left: 200.87799812568187px; }
78% { left: 200.89592530095737px; }
79% { left: 200.89665143457395px; }
80% { left: 200.88210931137806px; }
81% { left: 200.85427175192046px; }
82% { left: 200.81511366810776px; }
83% { left: 200.76657838029143px; }
84% { left: 200.7105482768328px; }
85% { left: 200.64881982187876px; }
86% { left: 200.58308284617112px; }
87% { left: 200.51490398641354px; }
88% { left: 200.44571408112546px; }
89% { left: 200.37679929488283px; }
90% { left: 200.3092956925791px; }
91% { left: 200.24418697437903px; }
92% { left: 200.1823050421591px; }
93% { left: 200.12433307616453px; }
94% { left: 200.07081078240043px; }
95% { left: 200.02214147494948px; }
96% { left: 199.97860066859386px; }
97% { left: 199.940345864654px; }
98% { left: 199.90742722728143px; }
99% { left: 199.8797988721998px; }
100% { left: 199.8573305048547px; }
}

Finally, you just use it wherever you want. Make sure you set the timing function to linear, as we already precomputed the animation.

.box {
animation: 1s move linear;
animation-fill-mode: both;
}

Customization

You can further customize the animation. For instance, you might want to try experimenting with different stiffness and damping factors.

You can also try changing the value of f'(0). Setting it to a negative number will cause the box to bounce in an opposite direction first.

You can also try multiplying the time with different factors to make it faster/slower. You can also experiment with bouncing animation instead of spring animation (use projectile motion and law of energy conservation).

And that’s it!

These are all simple application of physics and calculus in the world of design and programming.

When I studied Software Engineering, like every other engineering here, we had to study elementary calculus and physics. It’s common to hear people say: ‘I’m studying software engineer! What’s the point of learning this? Will I ever get to use them?’

In my opinion, it doesn’t matter whether you know how to solve differential equations or complicated physics problems or not; you could easily ask Google or Wolfram|Alpha to solve them for you

What matters in my opinion is whether you are able to use the knowledge as a tool to solve other problems that might interest you.

--

--

Thai Pangsakulyanont

(@dtinth) A software engineer and frontend enthusiast. A Christian. A JavaScript musician. A Ruby and Vim lover.