Learn Remotion
by Building
Create programmatic videos with React. Each lesson teaches a core concept through real, renderable compositions you can tweak and export.
Curriculum
🖥️ HelloRemotion
Every Remotion animation is a pure function of the frame number. Learn the two hooks that power everything.
The Two Essential Hooks
Remotion calls your React component once for every frame of the video. Your job is to return what that frame looks like.
// These two hooks are the foundation of everything
const frame = useCurrentFrame(); // 0, 1, 2, 3 ... 119
const { fps, width, height, durationInFrames } = useVideoConfig();
// fps=30, so 4 seconds = 120 frames
// width=1920, height=1080 (Full HD)hooks
useVideoConfig() — What It Returns
| Property | Type | Example | Use Case |
|---|---|---|---|
fps | number | 30 | Converting seconds to frames: 2 * fps = 60 frames |
width | number | 1920 | Centering elements: width / 2 |
height | number | 1080 | Positioning from bottom: height - 100 |
durationInFrames | number | 150 | Exit animations: durationInFrames - 30 |
interpolate() — Mapping Frames to Values
This is the core animation primitive. It maps a frame range to a value range:
// Fade in the title over the first 1 second (0 → 30 frames)
const titleOpacity = interpolate(
frame, // input value
[0, 1 * fps], // input range (frame 0 → frame 30)
[0, 1], // output range (opacity 0 → 1)
{ extrapolateRight: "clamp" } // don't go above 1
);
// Slide subtitle in from 60px right → 0
const subtitleX = interpolate(
frame,
[0.5 * fps, 1.5 * fps], // starts at 0.5s, ends at 1.5s
[60, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);interpolate
More interpolate() Examples
// Scale up from 0.5x to 1x (zoom in effect)
const scale = interpolate(frame, [0, 30], [0.5, 1], {
extrapolateRight: "clamp",
});
// Rotate 360° over 2 seconds (continuous spin)
const rotation = interpolate(frame, [0, 2 * fps], [0, 360]);
// No clamp! Keeps rotating past 360 if video is longer
// Multi-step: fade in → hold → fade out
const visibility = interpolate(
frame,
[0, 20, 40, 60], // 4 keyframes
[0, 1, 1, 0], // in → hold → out
);
// Time display: convert frame number to seconds
const timeInSeconds = (frame / fps).toFixed(1);
// frame=45, fps=30 → "1.5"more examples
Applying Values to JSX
return (
<AbsoluteFill style={{ backgroundColor }}>
<h1 style={{
opacity: titleOpacity,
transform: `translateY(${titleY}px) scale(${scale}) rotate(${rotation}deg)`,
}}>
{title}
</h1>
</AbsoluteFill>
);JSX
AbsoluteFill — The Layout Container
<AbsoluteFill> is the go-to container. It's a div that fills the entire composition:
// It's equivalent to:
<div style={{
position: "absolute",
top: 0, left: 0, right: 0, bottom: 0,
}}>
// Stack multiple layers:
<AbsoluteFill> {/* Background layer */}
<div style={{ background: "#0f172a" }} />
</AbsoluteFill>
<AbsoluteFill> {/* Content layer */}
<h1>On top!</h1>
</AbsoluteFill>AbsoluteFill
Every visual property is a pure function of the frame number. No CSS transitions, no setTimeout, no animate-* classes. Ever.
📂 File: src/lessons/1-fundamentals/HelloRemotion.tsx
⏱️ TimingPlayground
Not all motion is equal. Compare linear, eased, and spring-based animations side-by-side.
Linear vs Easing
interpolate() is linear by default, but you can add easing curves:
// Linear — constant speed (robotic)
const linear = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Eased — smooth acceleration and deceleration
const eased = interpolate(frame, [0, 2 * fps], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});easing
spring() — Physics-Based Motion
Springs simulate real-world physics. They output 0→1 with natural acceleration, overshoot, and settle:
const bouncy = spring({
frame,
fps,
config: {
damping: 10, // Lower = more bounce
stiffness: 100, // Higher = faster
mass: 1, // Higher = heavier/slower
},
});spring
Spring Presets
🎾 Bouncy
Playful, overshoots and bounces back
damping: 10, stiffness: 100
🧈 Smooth
Elegant, no overshoot at all
damping: 200, stiffness: 100
⚡ Snappy
Quick and responsive, slight bounce
damping: 20, stiffness: 200
🪨 Heavy
Slow, weighty, dramatic entrance
damping: 15, stiffness: 80, mass: 2
Open TimingPlayground in the Studio and change damping, stiffness, and mass in the sidebar. Watch all 4 boxes animate in real time!
📂 File: src/lessons/1-fundamentals/TimingPlayground.tsx
🎞️ SequencingDemo
Control when things happen. Sequence and Series are your timeline tools.
<Series> — One After Another
Series plays children sequentially, like slides in a presentation:
<Series>
<Series.Sequence durationInFrames={30} premountFor={30}>
<PanelA /> {/* Shows frames 0–29 */}
</Series.Sequence>
<Series.Sequence durationInFrames={30}>
<PanelB /> {/* Shows frames 30–59 */}
</Series.Sequence>
</Series>Series
Local Frames — The Key Insight
Inside a <Sequence>, useCurrentFrame() returns the local frame (starts at 0), not the global composition frame. This makes components reusable — they don't need to know when they start.
premountFor — Preloading
premountFor={1 * fps} mounts the component 1 second before it becomes visible. This gives it time to load fonts, images, or data without a visible delay.
Trimming with Negative from
// Trim the first 0.5 seconds off PanelA's animation
<Sequence from={-0.5 * fps} durationInFrames={1.5 * fps}>
<PanelA />
</Sequence>trimming
📂 File: src/lessons/1-fundamentals/SequencingDemo.tsx
⌨️ TypewriterEffect
Build a typewriter reveal with string slicing — the Remotion way to animate text.
Character-by-Character Reveal
The trick: use string.slice(0, charCount) where charCount grows with the frame:
const CHAR_FRAMES = 2; // 2 frames per character = 15 chars/sec
const charCount = Math.floor(frame / CHAR_FRAMES);
const displayedText = fullText.slice(0, charCount);
// Render it
<span>{displayedText}</span>typewriter
Blinking Cursor
Use modulo + interpolate for a repeating blink cycle:
const BLINK_FRAMES = 15;
const cursorOpacity = interpolate(
frame % BLINK_FRAMES,
[0, 7, 8, 15], // On for 7 frames, off for 7
[1, 1, 0, 0],
);cursor
Google Fonts
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
// Use it: style={{ fontFamily }}fonts
Don't animate text with per-character opacity — it's fragile and causes layout shifts. String slicing is the correct Remotion pattern.
📂 File: src/lessons/2-text/TypewriterEffect.tsx
✨ WordHighlight
Animate a highlight wipe across a keyword using spring-driven scaleX.
The Wipe Technique
Place a colored bar behind the word, then animate its scaleX from 0 to 1 with transformOrigin: left:
const highlightProgress = spring({
frame, fps,
config: { damping: 15, stiffness: 80 },
delay: Math.round(0.5 * fps), // Start after 0.5s
durationInFrames: 18,
});
// The highlight bar
<span style={{
position: "absolute",
bottom: 2,
left: -8, right: -8,
height: "40%",
backgroundColor: highlightColor,
transform: `scaleX(${highlightProgress})`,
transformOrigin: "left", // ← Wipes from left to right
}} />highlight wipe
Spring delay and durationInFrames
| Parameter | What it does |
|---|---|
delay |
Wait N frames before the spring starts moving |
durationInFrames |
Stretch/compress the spring to fit exactly N frames |
📂 File: src/lessons/2-text/WordHighlight.tsx
🖼️ MediaShowcase
Display images, embed videos, and play audio — all with Remotion's specialized components.
staticFile() — Loading from /public
import { staticFile } from "remotion";
// Always use staticFile() for public/ folder assets
const imageSrc = staticFile("photo.png");
const videoSrc = staticFile("clip.mp4");
const audioSrc = staticFile("music.mp3");staticFile
The Three Media Components
import { Img } from "remotion";
import { Video, Audio } from "@remotion/media";
// Images — waits for full load before rendering
<Img src={staticFile("photo.png")} />
// Video — with trimming and volume control
<Video
src={staticFile("clip.mp4")}
trimBefore={2 * fps} // Skip first 2 seconds
trimAfter={10 * fps} // End at 10 seconds
volume={0.5} // Half volume
playbackRate={1.5} // 1.5x speed
/>
// Audio — with dynamic volume curve
<Audio
src={staticFile("music.mp3")}
volume={(f) => interpolate(f, [0, 30], [0, 1], {
extrapolateRight: "clamp"
})}
/>media components
Never use native <img>, <video>, or CSS background-image. Remotion's components ensure assets are fully loaded before each frame is captured — preventing flicker during rendering.
📂 File: src/lessons/3-media/MediaShowcase.tsx
📊 AnimatedBarChart
Build an animated bar chart with staggered spring entries — a classic data viz pattern.
Staggered Springs
The key trick: offset each bar's spring by a few frames to create a cascading effect:
const STAGGER_DELAY = 5; // 5 frames between each bar
{values.map((value, i) => {
const barSpring = spring({
frame: frame - i * STAGGER_DELAY, // ← The magic
fps,
config: { damping: 18, stiffness: 80 },
});
const barHeight = (value / maxValue) * plotHeight * barSpring;
// barSpring goes 0→1, so barHeight grows from 0 to full
})}stagger
Zod Schema = Interactive Controls
export const AnimatedBarChartSchema = z.object({
chartTitle: z.string().default("Monthly Revenue"),
barColor: zColor().default("#d4af37"), // Color picker!
values: z.tuple([...]).default([45, 72, 58, 90, 65, 82]),
});schema
📂 File: src/lessons/4-dataviz/AnimatedBarChart.tsx
🥧 PieChartReveal
Animate SVG circle segments with strokeDasharray — the technique behind every pie chart animation.
The SVG Circle Trick
A pie chart is really a thick-stroked circle with dashes. Each dash = one segment:
const RADIUS = 120;
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
// Each segment's length is proportional to its value
const segmentLength = (value / total) * CIRCUMFERENCE;
<circle
r={RADIUS}
fill="none"
stroke={color}
strokeWidth={60}
strokeDasharray={`${animatedLength} ${CIRCUMFERENCE - animatedLength}`}
strokeDashoffset={-offset} {/* Position on the circle */}
/>SVG circle
How It Works
strokeDasharraycreates a visible dash of length N, then a gapstrokeDashoffsetrotates the dash to the right starting position- Animate the dash length with
spring()to make it "draw" transform: rotate(-90deg)starts drawing from 12 o'clock
📂 File: src/lessons/4-dataviz/PieChartReveal.tsx
🎭 SceneTransitions
Smooth transitions between scenes using <TransitionSeries> — Remotion's built-in transition system.
TransitionSeries
Like <Series>, but with cross-scene transitions between sequences:
import { TransitionSeries, linearTiming, springTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene1 />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 20 })}
/>
<TransitionSeries.Sequence durationInFrames={90}>
<Scene2 />
</TransitionSeries.Sequence>
</TransitionSeries>transitions
Available Transitions
| Effect | Import | Options |
|---|---|---|
| Crossfade | fade() | — |
| Slide | slide() | "from-left", "from-right", "from-top", "from-bottom" |
| Wipe | wipe() | Direction options |
| Flip | flip() | — |
| Clock Wipe | clockWipe() | — |
Duration Math
Transitions overlap both scenes, so: total = scene1 + scene2 − transitionDuration. A 20-frame fade between two 90-frame scenes = 160 total frames, not 180.
📂 File: src/lessons/5-advanced/SceneTransitions.tsx
🎓 KitchenSink
Your graduation project — a real video combining typewriter text, bar charts, and scene transitions.
Composition Architecture
A real-world Remotion video combines multiple techniques as scenes within a TransitionSeries:
Key Patterns Used
- Typewriter + highlight in the intro (Lessons 4-5)
- Staggered spring bar chart in the middle (Lesson 7)
- TransitionSeries with fade + slide between scenes (Lesson 9)
- Spring-animated particles in the celebratory outro
- Zod schema for customizable title, subtitle, and colors
Rendering Your Video
# Render to MP4
npx remotion render KitchenSink out/graduation.mp4
# Render a single frame as PNG
npx remotion still KitchenSink --frame=60 out/thumbnail.png
# With custom props
npx remotion render KitchenSink out/custom.mp4 \
--props='{"title":"My Video","accentColor":"#ef4444"}'CLI
You've learned: frames & interpolation, spring physics, sequencing, text animations, media handling, data visualization, and scene transitions. You're ready to build your own programmatic videos!
📂 File: src/lessons/5-advanced/KitchenSink.tsx
📖 Quick Reference
The essential Remotion APIs at a glance.
| API | Purpose | Import From |
|---|---|---|
useCurrentFrame() | Get the current frame number | remotion |
useVideoConfig() | Get fps, width, height, duration | remotion |
interpolate() | Map frame → value with optional easing | remotion |
spring() | Physics-based 0→1 animation | remotion |
Easing.* | Easing curves (quad, sin, bezier, etc.) | remotion |
<Sequence> | Delay/trim child with local timeline | remotion |
<Series> | Play children one after another | remotion |
<AbsoluteFill> | Full-size positioned container | remotion |
<Img> | Image (waits for load) | remotion |
staticFile() | Reference public/ folder assets | remotion |
<Video> | Video with trim/volume/speed | @remotion/media |
<Audio> | Audio with trim/volume/speed | @remotion/media |
<TransitionSeries> | Scenes with transitions | @remotion/transitions |
loadFont() | Google Fonts loader | @remotion/google-fonts/* |
z.object() | Parametric schemas | zod |
zColor() | Color picker in Studio sidebar | @remotion/zod-types |
CSS Properties for Remotion Creativity
Remotion forbids CSS animations (@keyframes, transition, animation).
Instead, you compute every property per-frame with interpolate() and spring().
The more CSS properties you know, the more creative effects you can build. Here's your toolkit.
CSS animations: "animate FROM this TO that over time"
Remotion: "at frame 42, what should this value BE?"
You're a puppet master — every frame is a snapshot, and you set every property yourself.
1. CSS Transform — Your Main Creative Tool
transform is the property you'll use most in Remotion.
It moves, scales, rotates, and skews elements — all on the GPU, so it's silky smooth.
The Five Transform Functions
Hover each card to see the effect ↓
transform: none;
/* on hover */
transform: translateX(15px) translateY(-10px);
transform: scale(1);
/* on hover */
transform: scale(1.4);
transform: rotate(0deg);
/* on hover */
transform: rotate(45deg);
transform: none;
/* on hover */
transform: skewX(15deg) skewY(5deg);
transform:
translateX(10px)
rotate(15deg)
scale(1.1);
Using Transforms in Remotion
// Move an element from off-screen to center
const frame = useCurrentFrame();
const translateX = interpolate(frame, [0, 30], [-500, 0], {
extrapolateRight: 'clamp',
});
// Scale up with spring physics
const scale = spring({ frame, fps, config: { damping: 12 } });
// Continuous rotation (no clamp — keeps spinning!)
const rotation = interpolate(frame, [0, 60], [0, 360]);
// Combine them all in one transform
return (
<div style={{
transform: `translateX(${translateX}px) scale(${scale}) rotate(${rotation}deg)`,
transformOrigin: 'center center', // pivot point!
}} />
);
transform-origin — The Pivot Point
Where the element rotates/scales from. Changes everything about the feel of an animation:
Center (default)
Standard zoom / spin effect
transformOrigin: 'center center'
Top-left
Element "unfolds" from its corner
transformOrigin: '0% 0%'
Bottom-center
Like a pendulum swinging from the top
transformOrigin: '50% 100%'
Custom point
Pivot from any pixel position
transformOrigin: '20px 80px'
translateX(100px) rotate(45deg) ≠ rotate(45deg) translateX(100px).
Transforms apply right-to-left. Rotate first, then translate → the element moves along the rotated axis!
Open 6-CSS-Mastery → TransformShowcase in Remotion Studio to see an orbiting solar system where each planet demonstrates translate, scale, rotate, and skew in real-time!
2. CSS clip-path — Reveals, Masks & Wipes
clip-path clips an element to a shape. Animate the shape values with
interpolate() to create stunning reveal effects, wipes, and morphing masks.
Shapes You Can Create
Hover each shape to see it morph ↓
/* hover → */ circle(35%);
50% 0%, 0% 100%, 100% 100%
);
50% 0%, 100% 50%,
50% 100%, 0% 50%
);
/* hover → */
inset(10% 20% 10% 20% round 12px);
50% 0%, 61% 35%, 98% 35%,
68% 57%, 79% 91%, ...
);
Remotion Reveal Patterns
// Circle reveal — expanding from center
const radius = interpolate(frame, [0, 30], [0, 70], {
extrapolateRight: 'clamp',
});
// clipPath: `circle(${radius}% at 50% 50%)`
// Horizontal wipe reveal (left to right)
const wipe = interpolate(frame, [0, 30], [100, 0], {
extrapolateRight: 'clamp',
});
// clipPath: `inset(0 ${wipe}% 0 0)`
// Diamond morph with spring
const s = spring({ frame, fps });
const size = interpolate(s, [0, 1], [50, 0]);
// clipPath: `polygon(50% ${size}%, ${100-size}% 50%, 50% ${100-size}%, ${size}% 50%)`
<div style={{
clipPath: `circle(${radius}% at 50% 50%)`,
background: 'url(your-image.jpg)',
width: '100%',
height: '100%',
}} />
Scene transitions: Wipe the next scene in with inset()
Logo reveals: Expand from center with circle()
Text masks: Clip text containers for a cinematic type-on effect
Progress bars: Use inset() to reveal from left to right
Open 6-CSS-Mastery → ClipPathReveal in Remotion Studio — 4 panels reveal using circle, diamond, inset wipe, and star burst clip-paths with spring physics!
3. CSS filter — Blur, Glow & Color Effects
filter applies visual effects like blur, brightness, and hue rotation.
In Remotion, animate these values for focus pulls, flash effects, and color grading transitions.
Filter Functions
Hover each card to see the filter ↓
/* hover → */ blur(4px);
/* hover → */ brightness(1.6);
/* hover → */ grayscale(1);
/* hover → */ hue-rotate(90deg);
/* hover → */ saturate(2.5);
/* hover → */ sepia(0.8);
/* hover → */ contrast(1.8);
/* hover → */ invert(1);
Remotion Filter Recipes
// Focus pull — start blurry, become sharp
const blurAmount = interpolate(frame, [0, 20], [8, 0], {
extrapolateRight: 'clamp',
});
// filter: `blur(${blurAmount}px)`
// Flash/white-out effect
const brightness = interpolate(frame, [0, 5, 15], [1, 3, 1], {
extrapolateRight: 'clamp',
});
// filter: `brightness(${brightness})`
// Color-to-grayscale transition
const gray = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: 'clamp',
});
// filter: `grayscale(${gray})`
// Psychedelic hue rotation (continuous spin)
const hue = interpolate(frame, [0, 60], [0, 360]);
// filter: `hue-rotate(${hue}deg)`
// Combine multiple filters!
<div style={{
filter: `blur(${blurAmount}px) brightness(${brightness})`,
}} />
backdropFilter: 'blur(10px)' blurs everything behind an element — perfect for
frosted glass overlays. Combine with a semi-transparent background for a glassmorphism effect!
Open 6-CSS-Mastery → FilterEffects — a split-screen color grading reel cycling through blur, grayscale, hue-rotate, and brightness+sepia with real-time filter value overlays!
4. SVG Basics — Paths, Circles & Drawing Effects
SVG gives you pixel-perfect vector shapes directly in JSX. Combined with Remotion's frame-based control, SVGs unlock path drawing animations, chart reveals, and custom shapes that scale to any resolution.
Core SVG Elements
The Magic Trick: strokeDasharray + strokeDashoffset
This is how you create "drawing" animations — making a line appear to draw itself:
// The "drawing" trick explained:
// 1. strokeDasharray = total path length (one giant dash)
// 2. strokeDashoffset = how much to HIDE (shift the dash)
// 3. Animate offset from length → 0 = "draws" the path
const pathLength = 314; // circumference of circle = 2πr
const frame = useCurrentFrame();
// Draw the circle over 60 frames
const drawProgress = interpolate(frame, [0, 60], [pathLength, 0], {
extrapolateRight: 'clamp',
});
<svg width="200" height="200">
<circle
cx="100" cy="100" r="50"
fill="none"
stroke="#c96442"
strokeWidth="4"
strokeDasharray={pathLength}
strokeDashoffset={drawProgress}
/>
</svg>
How strokeDashoffset works — the dash "slides" into view:
(0% drawn)
(25% drawn)
(50% drawn)
(75% drawn)
(100% drawn)
SVG Properties Cheat Sheet
stroke & strokeWidth
Outline color and thickness
stroke="#c96442" strokeWidth={3}
fill & fillOpacity
Interior color and transparency
fill="#c96442" fillOpacity={0.5}
strokeDasharray
Dash pattern: "dash,gap" or total length
strokeDasharray="10,5"
strokeDasharray={circumference}
strokeDashoffset
Shifts the dash pattern — the draw trick
strokeDashoffset={drawProgress}
strokeLinecap
"round" gives smooth line ends
strokeLinecap="round"
viewBox
Internal coordinate system for scaling
viewBox="0 0 200 200"
Real-world SVG + Remotion Example
// Animated pie chart slice (like your PieChartReveal lesson!)
const { fps } = useVideoConfig();
const frame = useCurrentFrame();
const slices = [
{ percent: 40, color: '#c96442', delay: 0 },
{ percent: 30, color: '#7c6b9e', delay: 10 },
{ percent: 30, color: '#4a8a80', delay: 20 },
];
let cumulativePercent = 0;
{slices.map((slice) => {
const radius = 80;
const circumference = 2 * Math.PI * radius;
const sliceLength = (slice.percent / 100) * circumference;
// Staggered spring animation
const progress = spring({
frame: frame - slice.delay,
fps,
config: { damping: 15 },
});
const offset = sliceLength * (1 - progress);
const rotation = (cumulativePercent / 100) * 360 - 90;
cumulativePercent += slice.percent;
return (
<circle
r={radius} cx="100" cy="100"
fill="none"
stroke={slice.color}
strokeWidth="40"
strokeDasharray={`${sliceLength} ${circumference}`}
strokeDashoffset={offset}
transform={`rotate(${rotation} 100 100)`}
/>
);
})}
Open 6-CSS-Mastery → SVGDrawing — watch a house blueprint draw itself line-by-line using strokeDashoffset, then fill with color. Includes a live progress bar!
5. More CSS Properties Worth Knowing
A few more properties that unlock creative possibilities in Remotion:
opacity
The simplest animation — fade in/out
const opacity = interpolate(frame, [0, 20], [0, 1]);
style={{ opacity }}
box-shadow
Glows, depth, neon effects
const glow = interpolate(frame, [0, 30], [0, 30]);
boxShadow: `0 0 ${glow}px #c96442`
border-radius
Morph between square and circle
const radius = interpolate(frame, [0, 30], [0, 50]);
borderRadius: `${radius}%`
background (gradient)
Animate gradient stops for color shifts
const pos = interpolate(frame, [0, 60], [0, 100]);
background: `linear-gradient(${pos}deg, ...)`
overflow: hidden
Container clips children — key for wipe reveals
// Parent clips, child translateX's in
overflow: 'hidden'
mix-blend-mode
Layer blending like Photoshop
mixBlendMode: 'screen'
// 'multiply', 'overlay', 'difference'
1. Pick a CSS property (transform, clip-path, filter, opacity, SVG attributes...)
2. Use interpolate(frame, inputRange, outputRange) to compute its value at each frame
3. Use spring({ frame, fps }) when you want natural, bouncy motion
4. Apply the computed value in your JSX style={{}} prop
That's it. Every Remotion animation is just: frame → math → CSS property.
@keyframes ·
transition ·
animation ·
animation-duration ·
Tailwind animate-* classes ·
setTimeout for animation timing
Remotion controls time. CSS must not.
interpolate() & spring() — The Animation Engine
These two functions power every single animation in Remotion. Master them and you can animate anything. Let's break them down completely.
📐 Part 1: interpolate()
interpolate() is a number translator.
Give it a number in one range, it gives you back a number in a different range.
That's it. But this simple idea is incredibly powerful.
Function Signature
frame from useCurrentFrame())
inputRange
Array of input checkpoints — must be strictly increasing (e.g., [0, 30, 60])
outputRange
Array of output values at each checkpoint (e.g., [0, 1, 0])
options
Optional: extrapolateLeft, extrapolateRight, easing
The Mental Model: A Translation Table
Think of it as mapping rows in a table. Remotion calculates values between the rows automatically:
| Frame (input) | → | Opacity (output) | What happens |
|---|---|---|---|
| 0 | → | 0 | Fully invisible |
| 15 | → | 0.5 | Half visible (calculated!) |
| 30 | → | 1 | Fully visible |
// This code produces the table above
const opacity = interpolate(frame, [0, 30], [0, 1]);
// frame=0 → 0
// frame=15 → 0.5 (interpolated automatically!)
// frame=30 → 1
Multi-Step Interpolation (Keyframes)
Use more than 2 values to create complex animation curves — like CSS keyframes but explicit:
// Fade in → hold → fade out
const opacity = interpolate(
frame,
[0, 20, 40, 60], // 4 checkpoints
[0, 1, 1, 0], // 4 values
);
// 0-20: fade in (0 → 1)
// 20-40: hold visible (1 → 1)
// 40-60: fade out (1 → 0)
| Frame | → | Opacity | Phase |
|---|---|---|---|
| 0 | → | 0 | Start invisible |
| 10 | → | 0.5 | Fading in… |
| 20 | → | 1 | Fully visible |
| 30 | → | 1 | Holding… |
| 40 | → | 1 | Still holding |
| 50 | → | 0.5 | Fading out… |
| 60 | → | 0 | Invisible again |
Extrapolation: What Happens Outside the Range?
By default, interpolate keeps going beyond your range (extrapolates). This is usually not what you want:
// ⚠️ WITHOUT clamp — keeps going past 1!
const bad = interpolate(frame, [0, 30], [0, 1]);
// frame=60 → 2.0 (oops! opacity > 1)
// ✅ WITH clamp — stops at the edges
const good = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: 'clamp', // stops at 1
extrapolateLeft: 'clamp', // stops at 0
});
// frame=60 → 1.0 (clamped!)
// frame=-5 → 0.0 (clamped!)
Almost always add extrapolateRight: 'clamp'. The only time you want extrapolation is for continuous animations like infinite rotation where values should keep growing.
Extrapolation Options
| Option | Behavior | Use Case |
|---|---|---|
'extend' (default) |
Continues the line beyond the range | Continuous rotation, infinite scroll |
'clamp' |
Stops at the edge value | Fade in/out, slide to position, most animations |
'identity' |
Returns the input value unchanged | Advanced: fallback to raw frame number |
Easing: Making Motion Feel Natural
Linear interpolation moves at constant speed. Easing curves make it accelerate and decelerate:
import { interpolate, Easing } from 'remotion';
// Linear (default) — constant speed, feels robotic
const linear = interpolate(frame, [0, 30], [0, 500]);
// Ease out — starts fast, decelerates (most natural for entrances)
const easeOut = interpolate(frame, [0, 30], [0, 500], {
easing: Easing.out(Easing.quad),
extrapolateRight: 'clamp',
});
// Ease in — starts slow, accelerates (good for exits)
const easeIn = interpolate(frame, [0, 30], [0, 500], {
easing: Easing.in(Easing.quad),
extrapolateRight: 'clamp',
});
// Ease in-out — slow start, fast middle, slow end
const easeInOut = interpolate(frame, [0, 30], [0, 500], {
easing: Easing.inOut(Easing.quad),
extrapolateRight: 'clamp',
});
Easing Curves Cheat Sheet
| Curve | Feel | Best For |
|---|---|---|
Easing.linear | Constant speed, robotic | Progress bars, tickers |
Easing.quad | Gentle acceleration | General-purpose motion |
Easing.cubic | More pronounced curve | Dramatic entrances |
Easing.sin | Smooth, wave-like | Breathing, pulsing |
Easing.exp | Extreme acceleration | Explosive reveals |
Easing.circle | Circular arc motion | Ball bouncing feel |
Easing.bezier(x1,y1,x2,y2) | Custom cubic bezier | Matching CSS transitions exactly |
Easing.in(Easing.quad) — slow start
Easing.out(Easing.quad) — slow end (most common for UI!)
Easing.inOut(Easing.quad) — slow start + slow end
Common interpolate() Recipes
Fade In
Simplest animation
interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' })
Slide from Left
Element enters from off-screen
interpolate(frame, [0, 30], [-500, 0], { easing: Easing.out(Easing.quad), extrapolateRight: 'clamp' })
Pulse (loop)
Continuous breathing effect
interpolate(frame % 60, [0, 30, 60], [1, 1.1, 1])
Typewriter Reveal
Show N characters
Math.floor(interpolate(frame, [0, 60], [0, text.length], { extrapolateRight: 'clamp' }))
Color Shift (via hue)
Rotate through the color wheel
interpolate(frame, [0, 90], [0, 360])
→ hue-rotate(${value}deg)
Blink / Toggle
Cursor blink, indicator flash
interpolate(frame % 30, [0, 14, 15, 29], [1, 1, 0, 0])
🌊 Part 2: spring()
spring() simulates a physics spring — like attaching a weight to a rubber band.
It goes from 0 to 1 with natural, organic motion.
No easing curve can replicate the overshoot and settle that a spring produces.
Function Signature
useCurrentFrame()). Subtract a number to delay the start.
fps
Frames per second (from useVideoConfig()). Required for physics simulation.
config
Physics params: { mass, damping, stiffness }. Controls the "feel".
durationInFrames
Optional: force the spring to fit exactly N frames (overrides natural duration).
The Physics Model — Explained Simply
Imagine a ball on a spring. Three knobs control how it moves:
Config Presets — Your Toolkit
🎯 Smooth (No Bounce)
Subtle reveals, fade-ins, gentle slides
config: { damping: 200 }
⚡ Snappy
UI elements, buttons, quick responses
config: { damping: 20, stiffness: 200 }
🏀 Bouncy
Playful animations, logos, attention-grabbing
config: { damping: 8 }
🏋️ Heavy
Slow, dramatic, weighty motion
config: { damping: 15, stiffness: 80, mass: 2 }
// Compare all 4 presets side by side
const smooth = spring({ frame, fps, config: { damping: 200 } });
const snappy = spring({ frame, fps, config: { damping: 20, stiffness: 200 } });
const bouncy = spring({ frame, fps, config: { damping: 8 } });
const heavy = spring({ frame, fps, config: { damping: 15, stiffness: 80, mass: 2 } });
// Each returns 0→1, but the "journey" feels totally different
Delaying a Spring
Subtract frames to delay the start. A negative frame just returns 0:
// Start immediately
const now = spring({ frame, fps });
// Start after 1 second (30 frames at 30fps)
const delayed = spring({ frame: frame - 30, fps });
// Stagger 5 items with 5-frame gaps
{items.map((item, i) => {
const s = spring({ frame: frame - i * 5, fps });
return <div style={{ opacity: s, transform: `translateY(${(1 - s) * 20}px)` }}>
{item}
</div>;
})}
Forcing a Duration
Springs have a natural duration based on physics. Override it when you need precise timing:
// Natural duration — takes as long as the physics dictates
const natural = spring({ frame, fps });
// Forced to exactly 1 second (30 frames)
const exact = spring({ frame, fps, durationInFrames: 30 });
// Useful for matching transitions, series timings, etc.
🔗 Part 3: Combining spring() + interpolate()
The real magic: spring() gives you 0→1,
then interpolate() maps that to any range.
This is the pattern you'll use 90% of the time:
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Step 1: Get a spring value (0 → 1)
const s = spring({ frame, fps, config: { damping: 12 } });
// Step 2: Map 0→1 to whatever you need
const scale = interpolate(s, [0, 1], [0.5, 1]); // 0.5 → 1
const y = interpolate(s, [0, 1], [100, 0]); // 100 → 0 (slide up)
const rotation = interpolate(s, [0, 1], [-45, 0]); // -45° → 0°
// Step 3: Apply all computed values
<div style={{
transform: `translateY(${y}px) scale(${scale}) rotate(${rotation}deg)`,
opacity: s, // spring value IS already 0→1, perfect for opacity!
}} />
1. spring() → creates natural 0→1 progress
2. interpolate(springValue, [0,1], [from, to]) → maps to your actual values
3. Apply in style={{}} → the element moves!
This 3-step pattern is the foundation of all Remotion animations.
Entrance + Exit Pattern
Combine two springs to make an element appear and then disappear:
const { fps, durationInFrames } = useVideoConfig();
const frame = useCurrentFrame();
// Entrance spring (0 → 1)
const enter = spring({ frame, fps });
// Exit spring (starts 1 second before end, goes 0 → 1)
const exit = spring({
frame: frame - (durationInFrames - 30),
fps,
config: { damping: 200 }, // smooth exit
});
// Subtract: enter brings it in, exit takes it out
const progress = enter - exit; // 0 → 1 → 0
const scale = interpolate(progress, [0, 1], [0.8, 1]);
<div style={{
opacity: progress,
transform: `scale(${scale})`,
}} />
🤔 When to Use Which?
| Situation | Use This | Why |
|---|---|---|
| Precise timing (exactly frame 20-40) | interpolate() |
You control the exact frame range |
| Natural-feeling entrance | spring() |
Physics simulation feels organic |
| Bounce / overshoot effect | spring() |
Only spring can overshoot and settle |
| Continuous animation (rotation, scroll) | interpolate() |
Spring settles at 1; interpolate keeps going |
| Staggered list items | spring(frame - i * delay) |
Each item gets its own delayed spring |
| Mapping 0→1 to a custom range | spring() + interpolate() |
Spring for the feel, interpolate for the range |
| Eased motion without bounce | Either works | interpolate + Easing.out or spring({ config: { damping: 200 } }) |
Open Remotion Studio (npm run studio) and explore:
• 1-Fundamentals → HelloRemotion — interpolate() for opacity, translateY
• 1-Fundamentals → TimingPlayground — spring() config comparison
• 6-CSS-Mastery → TransformShowcase — combined spring + interpolate for orbits
Every composition in this project uses these two functions. They're the engine of everything.
Scene-Based Video Framework
Build multi-scene videos where each section's duration is independently tunable — perfect for syncing animations to voiceover.
🧩 The Problem
You record a voiceover for a 3-scene video. Scene 2's audio is 4.2 seconds but your animation is only 3 seconds. How do you stretch just that scene without breaking everything?
🏗️ Architecture Overview
📦 File Structure
src/lib/
scene-types.ts — Types + Zod schema factory
ScenePlayer.tsx — Reusable <Series>-based renderer
calculate-scene-meta.ts — calculateMetadata helper
DebugPanel.tsx — Live value overlaystructure
1️⃣ Define Your Scenes
Each scene is a config object with a component, base duration, and extra frames knob:
import type { SceneConfig } from "./lib/scene-types";
const scenes: SceneConfig[] = [
{
id: "intro",
component: TitleScene,
baseDurationInFrames: 90, // 3 seconds at 30fps
extraFrames: 0, // ← Tunable via sidebar!
transcript: "Welcome to the show",
},
{
id: "main",
component: FeaturesScene,
baseDurationInFrames: 150,
extraFrames: 30, // +1 second to sync audio
audioFile: "voiceover/main.mp3",
},
{
id: "outro",
component: CodeScene,
baseDurationInFrames: 60,
extraFrames: 0,
},
];scene config
2️⃣ Create the Zod Schema
createScenesSchema() auto-generates extraFrames_* sliders for each scene in the Studio sidebar:
import { createScenesSchema } from "./lib/scene-types";
export const MyVideoSchema = createScenesSchema(
["intro", "main", "outro"], // scene IDs
{
backgroundColor: "#0f172a",
showCaptions: true,
captionColor: "#ffffff",
}
);
// This generates:
// extraFrames_intro: z.number().min(-300).max(600).default(0)
// extraFrames_main: z.number().min(-300).max(600).default(0)
// extraFrames_outro: z.number().min(-300).max(600).default(0)
// backgroundColor, showCaptions, captionColor, captionSizeschema factory
3️⃣ Wire Up calculateMetadata
This dynamically sizes the total composition duration as scenes are tuned:
import { createSceneMetadata } from "./lib/calculate-scene-meta";
const calculateMetadata = createSceneMetadata(scenes, 30);
// Formula per scene:
// duration = max(audioDuration, baseDuration) + extraFrames
//
// Total composition = sum of all scene durationscalculateMetadata
4️⃣ Register the Composition
<Composition
id="MyVideo"
component={MyVideoWrapper}
schema={MyVideoSchema}
defaultProps={{
extraFrames_intro: 0,
extraFrames_main: 0,
extraFrames_outro: 0,
showCaptions: true,
...
}}
durationInFrames={300} // placeholder — overridden by calculateMetadata
calculateMetadata={calculateMetadata}
fps={30}
width={1920}
height={1080}
/>registration
5️⃣ ScenePlayer Renders Everything
Your wrapper component passes the computed data to <ScenePlayer>:
import { ScenePlayer } from "./lib/ScenePlayer";
const MyVideoWrapper: React.FC = (props) => (
<ScenePlayer
scenes={scenes}
computedScenes={props._computedScenes}
showCaptions={props.showCaptions}
captionColor={props.captionColor}
/>
);wrapper
Next video you make: define new scenes, create a schema with createScenesSchema(), and wrap with <ScenePlayer>. Same framework, zero boilerplate.
🎛️ Tuning Workflow
- Open Remotion Studio → select your composition
- Play the video alongside your voiceover
- Scene 2 ends too early? Drag
extraFrames_mainslider to +30 - The composition auto-resizes — total duration recalculates
- Render when synced!
📊 Every Scene Gets a Debug Panel
The DebugPanel component is reusable across all compositions:
import { DebugPanel, type DebugEntry } from "./lib/DebugPanel";
const entries: DebugEntry[] = [
{ label: "frame", value: frame, color: "#f78c6c" },
{ label: "spring", value: mySpring, color: "#82aaff" },
{ label: "section", value: "intro", separator: true },
];
<DebugPanel
entries={entries}
title="My Values"
position="bottom-right" // or top-left, bottom-left, top-right
/>DebugPanel usage
Open the Studio and select 7-Scene-Framework → SceneSyncDemo. Try dragging the extraFrames_intro slider — watch the total composition duration change in real time!
📂 Files: src/lib/scene-types.ts, src/lib/ScenePlayer.tsx, src/lib/calculate-scene-meta.ts, src/lib/DebugPanel.tsx