remotion animation spring react tutorial

Remotion Spring Animation: The Complete Guide

Remotion Spring Animation: The Complete Guide

If you have ever watched a well-crafted mobile app and wondered why its animations feel so much more alive than a typical web page, spring physics is almost always the answer. A spring-based animation does not move at a fixed rate from point A to point B. Instead, it behaves like a physical spring: it accelerates, overshoots, bounces back, and eventually settles — producing motion that feels like it has weight and momentum.

Remotion, the React-based video programming library, ships first-class spring support through the spring() function. In this guide you will learn exactly how it works, how to tune it, and how to build production-ready animations like a bouncing logo entrance and a spring-driven animated counter.


What Is Spring Animation and Why Does It Matter in Video?

CSS and many animation libraries default to easing curves: ease-in, ease-out, cubic beziers. These are mathematically clean and completely predictable. The problem is that they can also look mechanical — as if the object knows in advance exactly where it needs to go and when it will arrive.

Spring animation is different because it is driven by simulated physics, not a predetermined curve. The animation does not know how long it will take; it runs until the forces acting on the spring reach equilibrium. This means:

  • Motion feels natural — the overshoot and settle sequence mirrors how physical objects actually behave
  • Parameters have intuitive meaning — you tune mass, stiffness, and damping rather than fiddling with bezier handles
  • Duration emerges from physics — you define the character of the motion, not an arbitrary millisecond count

For video specifically, spring animation is invaluable for title reveals, logo entrances, counter animations, progress bars, and any moment where you want the viewer to feel energy rather than watch a transition.


The Physics of Spring Motion

Before touching any code, it helps to understand the three parameters that control a spring’s behaviour. You do not need to understand the differential equations — just the intuition.

Mass

Mass is the weight of the object attached to the spring. A heavier object has more inertia: it takes longer to accelerate and longer to decelerate. In Remotion, the default mass is 1. Reducing it below 1 speeds the animation up; increasing it above 1 slows it down and produces more dramatic overshoot.

Stiffness

Stiffness is the tension of the spring itself — how forcefully it pulls the object back toward the target. Higher stiffness means a snappier, more responsive animation. The default is 100. Very high stiffness (e.g. 300) produces an almost instant snap. Very low stiffness (e.g. 20) produces a slow, floaty drift.

Damping

Damping is the braking force — the friction that prevents the spring from oscillating forever. Higher damping means the animation settles quickly with little or no bounce. Lower damping means more oscillation. The default is 10. Setting damping very high (e.g. 50) produces a smooth, zero-bounce ease-out. Setting it very low (e.g. 4) produces a dramatic bouncy effect.

The Interplay

These three forces interact. A high stiffness combined with low damping gives you a tight, vigorous bounce. A low stiffness combined with high damping gives you a gentle, cushioned ease. The best way to develop an intuition for this is to experiment — Remotion provides an interactive Spring Editor at springs.remotion.dev where you can drag sliders and watch the curve update in real time.


The spring() Function — Core API

Remotion exports spring() directly from the 'remotion' package. There is no separate package to install.

import { spring, useCurrentFrame, useVideoConfig } from 'remotion';

Function Signature

spring({
  frame,         // number — current frame (from useCurrentFrame)
  fps,           // number — frames per second (from useVideoConfig)
  config?: {
    mass?: number;           // default: 1
    stiffness?: number;      // default: 100
    damping?: number;        // default: 10
    overshootClamping?: boolean; // default: false
  };
  from?: number;           // default: 0 — starting value
  to?: number;             // default: 1 — ending value
  durationInFrames?: number; // constrain animation to this many frames
  delay?: number;          // delay onset by this many frames
})

Return Value

spring() returns a number. By default it moves from 0 to 1. When you pass from and to, it moves between those values directly — no need to post-process the output yourself.

Minimal Example

import { spring, useCurrentFrame, useVideoConfig } from 'remotion';

export const MyComp: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const scale = spring({
    frame,
    fps,
    from: 0,
    to: 1,
    config: { mass: 1, stiffness: 120, damping: 12 },
  });

  return (
    <div style={{ transform: `scale(${scale})` }}>
      Hello
    </div>
  );
};

At frame 0 the element is invisible (scale: 0). It grows past 1 briefly (overshoot), then settles at 1. That single bounce is what gives the motion its life.

The overshootClamping Option

Setting overshootClamping: true tells the spring to stop exactly at the to value without any overshoot. Use this when you are animating something where overshoot would look broken — a progress bar filling to 100%, for example, should not briefly show 103%.

const progress = spring({
  frame,
  fps,
  from: 0,
  to: 100,
  config: { overshootClamping: true, damping: 14 },
});

Using spring() with interpolate()

spring() produces a number in the fromto range, but often you want to map that number to a CSS value — a pixel distance, a rotation in degrees, or an opacity. The interpolate() function from 'remotion' is the right tool for this.

import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

export const SlideIn: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // spring goes 0 → 1
  const progress = spring({ frame, fps, config: { damping: 14, stiffness: 80 } });

  // map 0→1 to 200px → 0px (slide in from right)
  const translateX = interpolate(progress, [0, 1], [200, 0]);

  // map 0→1 to 0 → 1 opacity
  const opacity = interpolate(progress, [0, 1], [0, 1]);

  return (
    <div
      style={{
        transform: `translateX(${translateX}px)`,
        opacity,
      }}
    >
      Slide In
    </div>
  );
};

This pattern — spring() produces a normalised 0-to-1 driver, interpolate() maps it to a concrete CSS value — is the idiomatic Remotion approach and keeps your animation logic readable.


Practical Example 1: Bouncing Logo Entrance

A logo reveal is one of the most common use cases for spring animation. Here is a complete component that scales and fades a logo in with a satisfying bounce.

import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

export const LogoEntrance: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const enter = spring({
    frame,
    fps,
    config: {
      mass: 0.8,
      stiffness: 100,
      damping: 10,
    },
  });

  const scale = interpolate(enter, [0, 1], [0.4, 1]);
  const opacity = interpolate(enter, [0, 1], [0, 1], {
    extrapolateRight: 'clamp',
  });

  return (
    <div
      style={{
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        background: '#0f0f0f',
      }}
    >
      <img
        src="logo.svg"
        style={{
          transform: `scale(${scale})`,
          opacity,
          width: 240,
        }}
      />
    </div>
  );
};

With mass: 0.8 the logo feels light and energetic. The damping at 10 allows a single clean bounce. The extrapolateRight: 'clamp' on opacity ensures the value never exceeds 1 even during the spring overshoot.


Practical Example 2: Spring-Driven Animated Counter

Number counters that spring into place feel more dynamic than linear counters. Here the spring drives the displayed number, creating a natural overshoot-and-settle on the final value.

import { spring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';

interface CounterProps {
  targetValue: number;
  label: string;
}

export const SpringCounter: React.FC<CounterProps> = ({ targetValue, label }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const progress = spring({
    frame,
    fps,
    config: {
      mass: 1,
      stiffness: 60,
      damping: 12,
    },
  });

  // Map progress to a display value — Math.round for whole numbers
  const displayValue = Math.round(interpolate(progress, [0, 1], [0, targetValue]));

  return (
    <div style={{ textAlign: 'center', fontFamily: 'sans-serif', color: '#fff' }}>
      <div style={{ fontSize: 96, fontWeight: 700, lineHeight: 1 }}>
        {displayValue.toLocaleString()}
      </div>
      <div style={{ fontSize: 24, marginTop: 8, opacity: 0.7 }}>
        {label}
      </div>
    </div>
  );
};

The lower stiffness (60) gives the counter a gradual build-up before it snaps toward the final number. The overshoot means it briefly shows a value slightly above targetValue before settling — a subtle effect that makes the animation feel like the number has momentum.


Controlling Duration with durationInFrames

By default, spring() runs until it naturally settles — the physics determine the end time. Sometimes you need the animation to finish by a specific frame. The durationInFrames parameter constrains the spring to complete within that window.

const anim = spring({
  frame,
  fps,
  durationInFrames: fps * 0.5, // finish within half a second
  config: { stiffness: 200, damping: 20 },
});

Use measureSpring() — also exported from 'remotion' — when you need to calculate programmatically how long a given spring configuration will take to settle.

import { measureSpring } from 'remotion';

const frames = measureSpring({
  fps: 30,
  config: { mass: 1, stiffness: 100, damping: 10 },
});
// returns the number of frames until the spring is considered settled

This is useful when you want to chain animations: start the next element only after the previous spring has settled.


Delaying Spring Animations

The delay parameter shifts the animation’s start without requiring you to subtract from the frame yourself.

const delayed = spring({
  frame,
  fps,
  delay: fps * 0.3, // wait 0.3 seconds before starting
  config: { damping: 14 },
});

For staggered entrances — where multiple elements enter one after the other — you can pass an index-based delay to each element:

const items = ['Design', 'Develop', 'Deploy'];

// Inside your composition:
{items.map((item, i) => {
  const enter = spring({
    frame,
    fps,
    delay: i * 6, // each item starts 6 frames after the previous
    config: { damping: 14, stiffness: 90 },
  });
  const translateY = interpolate(enter, [0, 1], [40, 0]);
  const opacity = enter;

  return (
    <div
      key={item}
      style={{ transform: `translateY(${translateY}px)`, opacity }}
    >
      {item}
    </div>
  );
})}

Common Mistakes and How to Avoid Them

Passing frame without fps

spring() requires both frame and fps. Forgetting fps — or hardcoding it as a constant instead of reading it from useVideoConfig() — means your animation will run at the wrong speed when you change the composition’s frame rate.

Always destructure fps from useVideoConfig():

const { fps } = useVideoConfig();

Forgetting overshootClamping on bounded values

If you are animating a progress bar, an opacity, or any value that must stay within a strict range, always set overshootClamping: true or apply { extrapolateRight: 'clamp' } to your interpolate() call. A spring that overshoots opacity: 1 will briefly show opacity: 1.08, which is harmless visually — but a progress bar that hits 103% looks like a bug.

Using excessively low damping

Very low damping (below 6) creates many oscillation cycles that look unprofessional in most video contexts. Oscillating text is distracting; oscillating UI elements look broken. Start with damping around 10–14 and reduce only if you deliberately want a springy, playful feel.

Treating durationInFrames as an easing parameter

durationInFrames does not change how the spring feels — it only clips the animation to a window. If the physics produce an animation that naturally takes 40 frames but you set durationInFrames: 15, the spring will be compressed to fit. The output may feel unnaturally fast. If you want a faster animation, increase stiffness or reduce mass instead.

Not using the Spring Editor

A significant amount of time is wasted guessing spring parameters. Remotion’s interactive Spring Editor shows the curve in real time. Copy the parameters from there into your code and you will immediately have a curve that looks right.


FAQ

Q: Is spring() imported from 'remotion' or '@remotion/core'?

Both work. 'remotion' re-exports everything from '@remotion/core', so import { spring } from 'remotion' is the simpler and more common form. Use it unless you have a specific reason to reference the core package directly.

Q: Can I use spring animations inside a <Sequence>?

Yes. A <Sequence> offsets useCurrentFrame(), so the spring will naturally start when the sequence starts. No extra configuration needed — the frame the spring receives will be 0 at the sequence’s beginning.

Q: How do I make a spring animation that bounces exactly twice?

There is no direct parameter for “number of bounces.” Instead, lower damping until you see the desired number of oscillations, then fine-tune. The Spring Editor makes this iteration fast. A damping of roughly 5–7 with default mass and stiffness typically produces two to three visible bounces.

Q: What is the difference between spring() and CSS transition: spring?

CSS spring() easing (now available in modern browsers as linear() approximations) and Remotion’s spring() are conceptually similar but operate in different contexts. Remotion’s spring is evaluated server-side, frame by frame, as part of the video rendering pipeline. It produces deterministic, reproducible values. There is no browser interpolation involved.

Q: Can I animate multiple properties with the same spring?

Yes, and it is encouraged. A single progress value (from 0 to 1) can drive as many CSS properties as you like via separate interpolate() calls. This keeps all animated properties in sync and is more readable than managing multiple springs.

Q: My spring animation is visible in the preview but does not look right after rendering. Why?

This is almost always a frame rate mismatch. Make sure you pass the fps from useVideoConfig() — not a hardcoded number. If your composition is 30fps in the Remotion Studio preview but renders at 60fps, and you hardcoded fps: 30, the rendered animation will run at half speed.


Summary

Remotion’s spring() is a compact but powerful function. The core mental model is:

  1. spring() produces a number that moves from from to to with physics-based motion
  2. Combine it with interpolate() to map that number to any CSS value
  3. Tune mass, stiffness, and damping to control the character of the motion
  4. Use the Spring Editor to iterate fast

Spring animation is the single most effective technique for making programmatic video feel crafted rather than computed. Once you internalise the spring() + interpolate() pattern, you will use it in almost every composition you build.


Get production-ready spring animation templates at RenderComp

Every template in the RenderComp library is built with tuned spring configurations so you can drop in professional motion without starting from scratch. Browse logo entrances, stat counters, lower thirds, and more — all with open, editable source.

Ready to ship

Get 1,400+ Remotion Templates

Lifetime license. TypeScript-first. Ship polished video in minutes, not days.

Get RenderComp →