Remotion — Create Videos with React and TypeScript (2026 Tutorial)

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Videos, but make them React components. Remotion lets you create motion graphics, animated intros, data visualizations, and full video productions — entirely in TypeScript. No After Effects. No timeline editors. Just code.

What You Will Learn

By the end of this tutorial, you will:

  • Understand Remotion's architecture and core concepts
  • Set up a Remotion project from scratch with TypeScript
  • Build animated compositions using React components
  • Work with springs, interpolation, and audio
  • Create a data-driven video that renders dynamic content
  • Render your video as an MP4 file
  • Deploy a render pipeline for automated video generation

Prerequisites

Before starting, ensure you have:

  • Node.js 20+ installed (node --version)
  • React and TypeScript knowledge (hooks, components, JSX)
  • A code editor — VS Code recommended
  • ffmpeg installed for rendering (brew install ffmpeg on macOS)
  • Basic understanding of CSS animations and transforms

Why Remotion?

Traditional video editing tools are great for one-off projects. But what if you need to:

  • Generate hundreds of personalized videos for marketing campaigns
  • Create data-driven visualizations that update automatically
  • Build branded intros and outros that stay consistent across your content
  • Produce social media clips at scale with dynamic text overlays

Remotion solves this by treating video frames as React components. Each frame is a function of time — you write JSX, and Remotion renders it frame by frame into a real video file.

Key Concepts

ConceptDescription
CompositionA video definition with width, height, FPS, and duration
SequenceA time-shifted wrapper that delays when a component appears
useCurrentFrame()Hook that returns the current frame number
useVideoConfig()Hook that returns video metadata (width, height, fps, duration)
interpolate()Maps a frame range to an output range (like CSS keyframes)
spring()Physics-based animation curve for natural motion

Step 1: Project Setup

Create a new Remotion project using the official starter:

npx create-video@latest my-video --template blank
cd my-video

This scaffolds a TypeScript project with everything configured. Let's look at the structure:

my-video/
├── src/
│   ├── Root.tsx          # Entry point — registers compositions
│   ├── Composition.tsx   # Your first video component
│   └── index.ts          # Remotion entry
├── remotion.config.ts    # Remotion configuration
├── package.json
└── tsconfig.json

Install dependencies and start the preview server:

npm install
npm start

This opens the Remotion Studio at http://localhost:3000 — a browser-based preview environment where you can scrub through your video timeline in real time.


Step 2: Understanding the Composition

Open src/Root.tsx. This is where you register your video compositions:

import { Composition } from "remotion";
import { MyComposition } from "./Composition";
 
export const RemotionRoot: React.FC = () => {
  return (
    <>
      <Composition
        id="MyVideo"
        component={MyComposition}
        durationInFrames={150}
        fps={30}
        width={1920}
        height={1080}
      />
    </>
  );
};

Key properties:

  • id — Unique identifier used when rendering
  • durationInFrames — Total frames (150 frames at 30fps = 5 seconds)
  • fps — Frames per second (30 is standard, 60 for smooth motion)
  • width/height — Video resolution in pixels

Step 3: Building Your First Animation

Replace src/Composition.tsx with an animated title card:

import { useCurrentFrame, useVideoConfig, interpolate, spring } from "remotion";
import React from "react";
 
export const MyComposition: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  // Spring animation for the title
  const titleScale = spring({
    frame,
    fps,
    config: {
      damping: 12,
      stiffness: 200,
      mass: 0.5,
    },
  });
 
  // Fade in the subtitle after 20 frames
  const subtitleOpacity = interpolate(frame, [20, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
 
  // Slide the subtitle up
  const subtitleY = interpolate(frame, [20, 40], [30, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
 
  return (
    <div
      style={{
        flex: 1,
        background: "linear-gradient(135deg, #0f0c29, #302b63, #24243e)",
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        alignItems: "center",
        fontFamily: "Inter, sans-serif",
      }}
    >
      <h1
        style={{
          fontSize: 80,
          color: "white",
          fontWeight: 900,
          transform: `scale(${titleScale})`,
          textAlign: "center",
        }}
      >
        Hello, Remotion
      </h1>
 
      <p
        style={{
          fontSize: 32,
          color: "#a8a8ff",
          opacity: subtitleOpacity,
          transform: `translateY(${subtitleY}px)`,
          marginTop: 20,
        }}
      >
        Videos powered by React
      </p>
    </div>
  );
};

Save the file and check the Remotion Studio — you will see a smooth animation where the title bounces in with a spring effect, followed by a subtitle that fades and slides up.

How It Works

  1. useCurrentFrame() returns the current frame (0, 1, 2, ... 149)
  2. spring() creates a physics-based curve that starts at 0 and settles at 1
  3. interpolate() maps frame ranges to output values — frame 20-40 maps opacity from 0 to 1
  4. extrapolateLeft/Right: "clamp" prevents values from going below 0 or above 1

Step 4: Using Sequences for Timing

Sequences let you compose multiple animated sections with precise timing:

import {
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
  Sequence,
  AbsoluteFill,
} from "remotion";
import React from "react";
 
const Title: React.FC<{ text: string }> = ({ text }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  const scale = spring({ frame, fps, config: { damping: 12 } });
  const opacity = interpolate(frame, [0, 15], [0, 1], {
    extrapolateRight: "clamp",
  });
 
  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <h1
        style={{
          fontSize: 72,
          color: "white",
          transform: `scale(${scale})`,
          opacity,
          fontWeight: 800,
        }}
      >
        {text}
      </h1>
    </AbsoluteFill>
  );
};
 
const FadeOut: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const frame = useCurrentFrame();
  const opacity = interpolate(frame, [0, 10], [1, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
 
  return <AbsoluteFill style={{ opacity }}>{children}</AbsoluteFill>;
};
 
export const MyComposition: React.FC = () => {
  return (
    <AbsoluteFill
      style={{
        background: "linear-gradient(135deg, #0f0c29, #302b63, #24243e)",
      }}
    >
      {/* Scene 1: Intro (frames 0-59) */}
      <Sequence from={0} durationInFrames={60}>
        <Title text="Welcome" />
      </Sequence>
 
      {/* Scene 2: Main message (frames 45-119) */}
      <Sequence from={45} durationInFrames={75}>
        <Title text="Build Videos with Code" />
      </Sequence>
 
      {/* Scene 3: Outro with fade (frames 110-150) */}
      <Sequence from={110} durationInFrames={40}>
        <FadeOut>
          <Title text="Let's Go!" />
        </FadeOut>
      </Sequence>
    </AbsoluteFill>
  );
};

Notice how Sequence resets useCurrentFrame() to 0 within its children. This means each component starts its animation from frame 0, regardless of when it appears in the timeline. Sequences can overlap to create smooth transitions.


Step 5: Adding Audio

Remotion supports audio with precise frame-level synchronization:

import { Audio, Sequence, staticFile } from "remotion";
 
// Place your audio file in the /public folder
// e.g., public/music.mp3
 
export const MyComposition: React.FC = () => {
  return (
    <AbsoluteFill style={{ background: "#0f0c29" }}>
      {/* Background music starting at frame 0 */}
      <Audio src={staticFile("music.mp3")} volume={0.3} />
 
      {/* Sound effect at frame 30 */}
      <Sequence from={30}>
        <Audio src={staticFile("whoosh.mp3")} volume={0.8} />
      </Sequence>
 
      {/* Your visual content */}
      <Sequence from={0}>
        <Title text="With Sound!" />
      </Sequence>
    </AbsoluteFill>
  );
};

You can also dynamically control volume based on the current frame:

<Audio
  src={staticFile("music.mp3")}
  volume={(f) =>
    interpolate(f, [0, 30, 120, 150], [0, 0.5, 0.5, 0], {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
    })
  }
/>

This fades the music in over the first second and out over the last second.


Step 6: Data-Driven Videos

This is where Remotion really shines. Let's build a video that renders dynamic data — imagine generating weekly stats videos automatically:

import React from "react";
import {
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  spring,
  Sequence,
  AbsoluteFill,
} from "remotion";
 
interface StatsProps {
  title: string;
  stats: Array<{ label: string; value: number; color: string }>;
}
 
const AnimatedBar: React.FC<{
  label: string;
  value: number;
  maxValue: number;
  color: string;
  delay: number;
}> = ({ label, value, maxValue, color, delay }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  const progress = spring({
    frame: frame - delay,
    fps,
    config: { damping: 15, stiffness: 100 },
  });
 
  const width = interpolate(progress, [0, 1], [0, (value / maxValue) * 100]);
 
  const labelOpacity = interpolate(frame, [delay, delay + 10], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
 
  return (
    <div style={{ marginBottom: 24, opacity: labelOpacity }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          marginBottom: 8,
          fontSize: 24,
          color: "white",
          fontFamily: "Inter, sans-serif",
        }}
      >
        <span>{label}</span>
        <span style={{ fontWeight: 700 }}>
          {Math.round(value * progress)}
        </span>
      </div>
      <div
        style={{
          height: 32,
          background: "rgba(255,255,255,0.1)",
          borderRadius: 16,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            height: "100%",
            width: `${width}%`,
            background: `linear-gradient(90deg, ${color}, ${color}aa)`,
            borderRadius: 16,
          }}
        />
      </div>
    </div>
  );
};
 
export const StatsVideo: React.FC<StatsProps> = ({ title, stats }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
 
  const titleScale = spring({ frame, fps, config: { damping: 12 } });
  const maxValue = Math.max(...stats.map((s) => s.value));
 
  return (
    <AbsoluteFill
      style={{
        background: "linear-gradient(180deg, #1a1a2e, #16213e)",
        padding: 80,
        justifyContent: "center",
      }}
    >
      <h1
        style={{
          fontSize: 56,
          color: "white",
          fontWeight: 900,
          marginBottom: 60,
          transform: `scale(${titleScale})`,
          fontFamily: "Inter, sans-serif",
        }}
      >
        {title}
      </h1>
 
      {stats.map((stat, i) => (
        <AnimatedBar
          key={stat.label}
          label={stat.label}
          value={stat.value}
          maxValue={maxValue}
          color={stat.color}
          delay={i * 15 + 10}
        />
      ))}
    </AbsoluteFill>
  );
};

Register the composition with default props in Root.tsx:

<Composition
  id="WeeklyStats"
  component={StatsVideo}
  durationInFrames={180}
  fps={30}
  width={1080}
  height={1080}
  defaultProps={{
    title: "This Week's Performance",
    stats: [
      { label: "New Users", value: 1247, color: "#6366f1" },
      { label: "Page Views", value: 8432, color: "#22d3ee" },
      { label: "Conversions", value: 342, color: "#f43f5e" },
      { label: "Revenue ($)", value: 5890, color: "#10b981" },
    ],
  }}
/>

Now you have a reusable stats video component. Change the props and you get a completely different video — perfect for automation.


Step 7: Loading Custom Fonts

Use @remotion/google-fonts or load fonts manually:

npm install @remotion/google-fonts
import { loadFont } from "@remotion/google-fonts/Inter";
 
const { fontFamily } = loadFont();
 
// Use in your components
<h1 style={{ fontFamily }}>Styled with Inter</h1>

For local fonts, place them in the public/ folder and use staticFile():

import { staticFile } from "remotion";
 
const fontFace = new FontFace("MyFont", `url(${staticFile("fonts/MyFont.woff2")})`);
 
// Load in your component
React.useEffect(() => {
  document.fonts.add(fontFace);
  fontFace.load();
}, []);

Step 8: Rendering Your Video

Preview Rendering

The Remotion Studio lets you preview in real time. Use the timeline to scrub, and the play button to watch at full speed.

CLI Rendering

Render your video as an MP4 from the command line:

npx remotion render MyVideo out/my-video.mp4

Options you can customize:

npx remotion render MyVideo out/my-video.mp4 \
  --codec h264 \
  --quality 80 \
  --concurrency 4
FlagDescription
--codech264 (MP4), vp8/vp9 (WebM), prores (MOV)
--quality0-100, JPEG quality for frames
--concurrencyNumber of parallel rendering threads
--propsJSON string to override default props

Rendering with Custom Props

Pass data at render time to create different videos from the same template:

npx remotion render WeeklyStats out/stats-week-14.mp4 \
  --props='{"title":"Week 14 Stats","stats":[{"label":"Users","value":2000,"color":"#6366f1"}]}'

Step 9: Rendering via Node.js API

For automated pipelines, use the programmatic API:

import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import path from "path";
 
async function renderVideo() {
  // Bundle the Remotion project
  const bundleLocation = await bundle({
    entryPoint: path.resolve("./src/index.ts"),
  });
 
  // Select the composition
  const composition = await selectComposition({
    serveUrl: bundleLocation,
    id: "WeeklyStats",
    inputProps: {
      title: "April 2026 Report",
      stats: [
        { label: "Revenue", value: 12500, color: "#10b981" },
        { label: "Users", value: 3400, color: "#6366f1" },
      ],
    },
  });
 
  // Render to MP4
  await renderMedia({
    composition,
    serveUrl: bundleLocation,
    codec: "h264",
    outputLocation: "out/april-report.mp4",
    onProgress: ({ progress }) => {
      console.log(`Rendering: ${Math.round(progress * 100)}%`);
    },
  });
 
  console.log("Video rendered successfully!");
}
 
renderVideo();

This is perfect for:

  • CI/CD pipelines that generate release announcement videos
  • Cron jobs that produce daily/weekly report videos
  • API endpoints that create personalized videos on demand

Step 10: Advanced Techniques

Easing Functions

Remotion provides built-in easing curves:

import { Easing, interpolate } from "remotion";
 
const value = interpolate(frame, [0, 30], [0, 1], {
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
  extrapolateRight: "clamp",
});

Staggered Animations

Create staggered entrance effects by offsetting delays:

const items = ["React", "TypeScript", "Remotion"];
 
return (
  <>
    {items.map((item, i) => {
      const delay = i * 10;
      const opacity = interpolate(frame, [delay, delay + 15], [0, 1], {
        extrapolateLeft: "clamp",
        extrapolateRight: "clamp",
      });
      const x = interpolate(frame, [delay, delay + 15], [-50, 0], {
        extrapolateLeft: "clamp",
        extrapolateRight: "clamp",
      });
 
      return (
        <p
          key={item}
          style={{
            opacity,
            transform: `translateX(${x}px)`,
            fontSize: 36,
            color: "white",
          }}
        >
          {item}
        </p>
      );
    })}
  </>
);

Using Images and Videos

Remotion can composite images and embed other videos:

import { Img, Video, staticFile } from "remotion";
 
// Static image
<Img src={staticFile("logo.png")} style={{ width: 200 }} />
 
// Embed a video
<Video src={staticFile("background.mp4")} />
 
// Remote image (fetched during render)
<Img src="https://example.com/chart.png" />

Responsive Sizing

Use useVideoConfig() for responsive layouts that work across different resolutions:

const { width, height } = useVideoConfig();
 
<div
  style={{
    fontSize: width * 0.04, // 4% of video width
    padding: width * 0.05,  // 5% padding
  }}
>
  Scales with resolution
</div>

Testing Your Implementation

  1. Start the preview server: npm start and verify all animations play correctly
  2. Check timing: Use the timeline scrubber to verify sequence transitions
  3. Test rendering: Run npx remotion render MyVideo out/test.mp4 and play the output
  4. Verify audio sync: If using audio, check that sound effects align with visual cues
  5. Test with different props: Pass various data sets to your data-driven compositions

Troubleshooting

Common Issues

"ffmpeg not found" Install ffmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Ubuntu).

Slow rendering Increase concurrency with --concurrency 8. Each thread renders frames in parallel.

Fonts not loading Ensure fonts are loaded before the first frame renders. Use @remotion/google-fonts for reliable font loading, or preload custom fonts with delayRender():

import { delayRender, continueRender } from "remotion";
 
const [handle] = React.useState(() => delayRender());
 
React.useEffect(() => {
  const font = new FontFace("MyFont", `url(${staticFile("font.woff2")})`);
  font.load().then(() => {
    document.fonts.add(font);
    continueRender(handle);
  });
}, [handle]);

Black frames at the start This usually means an async resource (image, font, data) has not loaded yet. Use delayRender() and continueRender() to pause rendering until everything is ready.


Project Ideas

Now that you know the basics, here are some ideas to build:

  1. YouTube intro/outro templates — Branded animations for your channel
  2. Social media story generator — Dynamically fill product info into story templates
  3. Code walkthrough videos — Animate code appearing line by line with syntax highlighting
  4. Event countdown timers — Animated countdown to a launch date
  5. Invoice/receipt videos — Animated financial summaries for clients
  6. Podcast audiograms — Waveform visualizations synced to podcast audio

Next Steps

  • Explore the Remotion documentation for advanced features like <OffthreadVideo>, Lambda rendering, and Player embedding
  • Check out Remotion templates for production-ready starting points
  • Learn about Remotion Lambda for rendering videos at scale on AWS
  • Combine with Motion (Framer Motion) for complex UI animations inside your videos

Conclusion

Remotion transforms video creation from a manual, creative-tool-heavy process into a programmable, repeatable, and scalable workflow. By treating every frame as a React component, you get the full power of TypeScript, npm packages, and component composition — applied to video.

Whether you are building automated marketing content, data visualizations, or branded templates, Remotion gives you the developer experience you already know and love. Start with simple animations, compose them into sequences, and before long you will be rendering production-quality videos entirely from code.


Want to read more tutorials? Check out our latest tutorial on Mastra AI Framework: Build Intelligent Agents & Workflows in TypeScript.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles

Building Local-First Collaborative Apps with Yjs and React

Learn how to build real-time collaborative applications that work offline using Yjs CRDTs and React. This tutorial covers conflict-free data synchronization, offline-first architecture, and building a shared document editor from scratch.

30 min read·