🎬 10 Hands-On Lessons

Learn Remotion
by Building

Create programmatic videos with React. Each lesson teaches a core concept through real, renderable compositions you can tweak and export.

Lesson 1 — Fundamentals

🖥️ 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

PropertyTypeExampleUse Case
fpsnumber30Converting seconds to frames: 2 * fps = 60 frames
widthnumber1920Centering elements: width / 2
heightnumber1080Positioning from bottom: height - 100
durationInFramesnumber150Exit 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
Frame 0opacity: 0
Frame 15opacity: 0.5
Frame 30opacity: 1.0

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
🔑 The Golden Rule

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

Lesson 2 — Fundamentals

⏱️ 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
💡 Try It Live

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

Lesson 3 — Fundamentals

🎞️ 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
Panel 1
Panel 2
Panel 3
Panel 4
0s1s2s3s4s

Local Frames — The Key Insight

📌 Important

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

Lesson 4 — Text & Typography

⌨️ 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
🚫 Never Do This

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

Lesson 5 — Text & Typography

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

Lesson 6 — Media & Assets

🖼️ 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
🚫 Forbidden

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

Lesson 7 — Data Visualization

📊 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
Bar 1frame - 0
Bar 2frame - 5
Bar 3frame - 10
Bar 4frame - 15
Bar 5frame - 20
Bar 6frame - 25

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

Lesson 8 — Data Visualization

🥧 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

  1. strokeDasharray creates a visible dash of length N, then a gap
  2. strokeDashoffset rotates the dash to the right starting position
  3. Animate the dash length with spring() to make it "draw"
  4. transform: rotate(-90deg) starts drawing from 12 o'clock

📂 File: src/lessons/4-dataviz/PieChartReveal.tsx

Lesson 9 — Advanced

🎭 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

EffectImportOptions
Crossfadefade()
Slideslide()"from-left", "from-right", "from-top", "from-bottom"
Wipewipe()Direction options
Flipflip()
Clock WipeclockWipe()

Duration Math

📐 Important

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

Lesson 10 — The Finale

🎓 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:

Intro Scene
fade
Chart Scene
slide
Outro Scene
Typewriter + Highlight Staggered Bars Particle Burst 🎉

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
🎉 Congratulations!

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.

APIPurposeImport From
useCurrentFrame()Get the current frame numberremotion
useVideoConfig()Get fps, width, height, durationremotion
interpolate()Map frame → value with optional easingremotion
spring()Physics-based 0→1 animationremotion
Easing.*Easing curves (quad, sin, bezier, etc.)remotion
<Sequence>Delay/trim child with local timelineremotion
<Series>Play children one after anotherremotion
<AbsoluteFill>Full-size positioned containerremotion
<Img>Image (waits for load)remotion
staticFile()Reference public/ folder assetsremotion
<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 schemaszod
zColor()Color picker in Studio sidebar@remotion/zod-types
🎨 Bonus Section

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.

⚡ Key Mindset Shift

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 ↓

translate
DefaultBox sits at its original position
HoverShifts right 15px and up 10px
/* default */
transform: none;
/* on hover */
transform: translateX(15px) translateY(-10px);
scale
DefaultNormal size (scale 1.0)
HoverGrows to 1.4× its original size
/* default */
transform: scale(1);
/* on hover */
transform: scale(1.4);
rotate
DefaultNo rotation (0 degrees)
HoverSpins 45° clockwise around center
/* default */
transform: rotate(0deg);
/* on hover */
transform: rotate(45deg);
skew
DefaultNormal rectangular shape
HoverSlants into a parallelogram
/* default */
transform: none;
/* on hover */
transform: skewX(15deg) skewY(5deg);
combined
DefaultStatic box, no transforms
HoverMoves, rotates, and scales at once
/* on hover — all 3 at once! */
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'
💡 Pro Tip: Transform Order Matters

translateX(100px) rotate(45deg)rotate(45deg) translateX(100px).
Transforms apply right-to-left. Rotate first, then translate → the element moves along the rotated axis!

🎬 See it Live in Studio

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 ↓

circle()
DefaultFull circle, 50% radius
HoverShrinks to 35% — like zoom focus
clip-path: circle(50%);
/* hover → */ circle(35%);
polygon() — Triangle
DefaultFull triangle touching edges
HoverShrinks inward, points retract
clip-path: polygon(
  50% 0%, 0% 100%, 100% 100%
);
polygon() — Diamond
DefaultDiamond shape, corners at edges
HoverCorners pull inward 10%
clip-path: polygon(
  50% 0%, 100% 50%,
  50% 100%, 0% 50%
);
inset()
DefaultNo inset — full rectangle shown
HoverCrops 10% top/bottom, 20% sides + rounded
clip-path: inset(0);
/* hover → */
inset(10% 20% 10% 20% round 12px);
polygon() — Star
Default10-point star, sharp tips
HoverTips retract — star gets rounder
clip-path: polygon(
  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%',
}} />
🎬 Real-world Use Cases

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

🎬 See it Live in Studio

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 ↓

🏔️
blur()
DefaultImage is sharp and clear
HoverBlurs by 4px — like out-of-focus
filter: blur(0px);
/* hover → */ blur(4px);
☀️
brightness()
DefaultNormal brightness (1.0)
HoverBlown out to 1.6× — flash effect
filter: brightness(1);
/* hover → */ brightness(1.6);
🎨
grayscale()
DefaultFull color (grayscale 0)
HoverAll color stripped — black & white
filter: grayscale(0);
/* hover → */ grayscale(1);
🌀
hue-rotate()
DefaultOriginal colors (0° rotation)
HoverShifts hue 90° — warm → cool
filter: hue-rotate(0deg);
/* hover → */ hue-rotate(90deg);
💎
saturate()
DefaultNormal saturation (1.0)
HoverVivid, punchy colors at 2.5×
filter: saturate(1);
/* hover → */ saturate(2.5);
📜
sepia()
DefaultNormal colors, no sepia
HoverWarm vintage/old-photo look
filter: sepia(0);
/* hover → */ sepia(0.8);
contrast()
DefaultNormal contrast (1.0)
HoverDramatic darks/lights at 1.8×
filter: contrast(1);
/* hover → */ contrast(1.8);
🔄
invert()
DefaultNormal colors (not inverted)
HoverNegative image — all colors flipped
filter: invert(0);
/* 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})`,
}} />
💡 Pro Tip: backdrop-filter

backdropFilter: 'blur(10px)' blurs everything behind an element — perfect for frosted glass overlays. Combine with a semi-transparent background for a glassmorphism effect!

🎬 See it Live in Studio

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

<circle> <rect> <line> <path> <polygon>

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:

offset = 251
(0% drawn)
offset = 188
(25% drawn)
offset = 125
(50% drawn)
offset = 63
(75% drawn)
offset = 0
(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)`}
    />
  );
})}
🎬 See it Live in Studio

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'
🎯 Summary: The Remotion Creative Formula

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.

🚫 Remember: Never Use These in Remotion

@keyframes · transition · animation · animation-duration · Tailwind animate-* classes · setTimeout for animation timing
Remotion controls time. CSS must not.

🧠 Deep Dive

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

interpolate(input, inputRange, outputRange, options?)
input The current value (usually 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
00Fully invisible
150.5Half visible (calculated!)
301Fully 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)
FrameOpacityPhase
00Start invisible
100.5Fading in…
201Fully visible
301Holding…
401Still holding
500.5Fading out…
600Invisible 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!)
⚡ Rule of Thumb

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

OptionBehaviorUse 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

CurveFeelBest For
Easing.linearConstant speed, roboticProgress bars, tickers
Easing.quadGentle accelerationGeneral-purpose motion
Easing.cubicMore pronounced curveDramatic entrances
Easing.sinSmooth, wave-likeBreathing, pulsing
Easing.expExtreme accelerationExplosive reveals
Easing.circleCircular arc motionBall bouncing feel
Easing.bezier(x1,y1,x2,y2)Custom cubic bezierMatching CSS transitions exactly
💡 Pro Tip: Wrap curves with in/out/inOut

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

spring({ frame, fps, config?, durationInFrames? })
frame Current frame (from 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:

mass Default: 1
How heavy the ball is. Heavier = slower, more overshoot. Like a bowling ball vs tennis ball.
damping Default: 10
How much friction/resistance. Low = lots of bounce. High = smooth, no bounce. Like air vs honey.
stiffness Default: 100
How tight the spring is. Higher = faster snap to target. Like a rubber band vs bungee cord.

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!
}} />
⚡ The Pattern: spring → interpolate → style

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?

SituationUse ThisWhy
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 } })
🎬 See It All in Action

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.

🎬 Framework

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

SceneConfig[]scenes + baseDuration + extraFrames
calculateMetadatacomputes total duration
ScenePlayerrenders via <Series>

📦 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
💡 Reusability

Next video you make: define new scenes, create a schema with createScenesSchema(), and wrap with <ScenePlayer>. Same framework, zero boilerplate.

🎛️ Tuning Workflow

  1. Open Remotion Studio → select your composition
  2. Play the video alongside your voiceover
  3. Scene 2 ends too early? Drag extraFrames_main slider to +30
  4. The composition auto-resizes — total duration recalculates
  5. 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
🎥 See it Live in Studio

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