DEV Community

Cover image for How to create a spring animation with Web Animation API
Kirill Vasiltsov
Kirill Vasiltsov

Posted on • Originally published at

How to create a spring animation with Web Animation API

In this article I explain how to create animations with Web Animation API using springs (or rather, physics behind them).


Spring physics sounds intimidating, and that's what kept me from using it in my own animation projects. But as this awesome article by Maxime Heckel shows, you probably already know some of it and the rest isn't very complicated. If you haven't read the article yet, you should read it now, because everything below assumes you know the principles. If you are not familiar with Web Animation API, start here.

Quick recap

For convenience, here's a quick recap:

  • Springs have stiffness, mass and a damping ratio (also length but it is irrelevant here).
  • One force that acts on a spring when you displace it is:
F = -k * x // where k is stiffness and x is displacement
Enter fullscreen mode Exit fullscreen mode
  • Another force is damping force. It slows the spring down so it eventually stops:
F = -d * v // where d is damping ratio and v is velocity
Enter fullscreen mode Exit fullscreen mode
  • If we know acceleration and a time interval, we can calculate speed from previous speed:
v2 = v1 + a*t
Enter fullscreen mode Exit fullscreen mode
  • If we know speed and a time interval, we can calculate position from previous position and speed:
p2 =  p1 + v*t
Enter fullscreen mode Exit fullscreen mode


Here's the Codesandbox that shows the final result. You can play with it and change some default parameters.


First of all, we need some listeners.

  • mousedown and mousemove to start tracking the displacement of the square
  • mouseup to calculate and play an animation (more on that below)

This is pretty straightforward, so I am going to omit the details.

Drag transform

Strictly speaking, we are not dragging the element using native browser API. But we want to make it look like we move it! To do that, we set a CSS transform string directly to the element on each mousemove event.

function transformDrag(dx, dy) { = `translate(${dx}px, ${dy}px)`;

function handleMouseMove(e) {
  const dx = e.clientX - mouseX;
  const dy = e.clientY - mouseY;
  dragDx = dragDx + dx;
  dragDy = dragDy + dy;
  transformDrag(dragDx, dragDy);
Enter fullscreen mode Exit fullscreen mode

Generating keyframes

Now, the most important part of the animation. When we release (mouseup) the square, we need to animate how it goes back to its original position. But to make it look natural, we use a spring.

Any animation that uses WAAPI requires a set of keyframes which are just like the keyframes you need for a CSS animation. Only in this case, each keyframe is a Javascript object. Our task here is to generate an array of such objects and launch the animation.

We need a total of 5 parameters to be able to generate keyframes:

  1. Displacement on x-axis
  2. Displacement on y-axis
  3. Stiffness
  4. Mass
  5. Damping ratio

In the codesandbox above we use these defaults for physical parameters 3-5: 600, 7 and 1. For simplicity, we assume that the spring has length 1.

function createSpringAnimation(
        stiffness = 600,
        damping = 7,
        mass = 1
      ) {
        const spring_length = 1;
        const k = -stiffness;
        const d = -damping;
        // ...
Enter fullscreen mode Exit fullscreen mode

dx and dy are dynamic: we will pass them to the function on mouseup event.

A time interval in the context of the browser is one frame, or ~0.016s.

const frame_rate = 1 / 60;
Enter fullscreen mode Exit fullscreen mode

To generate one keyframe we simply apply the formulas from the article above:

let x = dx;
let y = dy;

let velocity_x = 0;
let velocity_y = 0;

let Fspring_x = k * (x - spring_length);
let Fspring_y = k * (y - spring_length);
let Fdamping_x = d * velocity_x;
let Fdamping_y = d * velocity_y;

let accel_x = (Fspring_x + Fdamping_x) / mass;
let accel_y = (Fspring_y + Fdamping_y) / mass;

velocity_x += accel_x * frame_rate;
velocity_y += accel_y * frame_rate;

x += velocity_x * frame_rate;
y += velocity_y * frame_rate;

const keyframe = { transform: `translate(${x}px, ${y}px)` }
Enter fullscreen mode Exit fullscreen mode

Ideally we need a keyframe for each time interval to have a smooth 60fps animation. Intuitively, we need to loop until the end of animation duration (duration divided by one frame length times). There's a problem, however - we don't know when exactly the spring will stop beforehand! This is the biggest difficulty when trying to animate springs with browser APIs that want the exact duration time from you. Fortunately, there is a workaround: loop a potentially large number of times, but break when we have enough keyframes. Let's say we want it to stop when the largest movement does not exceed 3 pixels (in both directions) for the last 60 frames - simply because it becomes not easy to notice movement. We lose precision but reach the goal.

So, this is what this heuristic looks like in code:


let frames = 0;
let frames_below_threshold = 0;
let largest_displ;

let positions = [];

for (let step = 0; step <= 1000; step += 1) {
  // Generate a keyframe
  // ...
  // Put the keyframe in the array

  largest_displ =
    largest_displ < 0
      ? Math.max(largest_displ || -Infinity, x)
      : Math.min(largest_displ || Infinity, x);

  if (Math.abs(largest_displ) < DISPL_THRESHOLD) {
     frames_below_threshold += 1;
  } else {
     frames_below_threshold = 0; // Reset the frame counter

  if (frames_below_threshold >= 60) {
     frames = step;
Enter fullscreen mode Exit fullscreen mode

After we break, we save the number of times we looped as the number of frames. We use this number to calculate the actual duration. This is the mouseup handler:

let animation;

function handleMouseUp(e) {
   const { positions, frames } = createSpringAnimation(dragDx, dragDy); = ""; // Cancel all transforms right before animation

   const keyframes = new KeyframeEffect(square, positions, {
          duration: (frames / 60) * 1000,
          fill: "both",
          easing: "linear",
          iterations: 1

   animation = new Animation(keyframes);;
Enter fullscreen mode Exit fullscreen mode

Note that the easing option of the animation is set to linear because we already solve it manually inside the createSpringAnimation function.

This is all you need to generate a nice smooth 60fps spring animation!

Discussion (1)

okikio profile image
Okiki Ojo

Awesome article. Inspired by your article, I was able to add Custom Easing to the Web Animation API, check out a demo below:

I built the Custom Easing functionality into @okikio/animate. (@okikio/animate is an animation library which uses the Web Animation API to create fluid animations. If you want to learn more about @okikio/animate I suggest reading the article I wrote on CSS-Tricks about it,

I wrote a detailed article on adding custom easing to WAAPI on