Create an Animated Social Media Video

Build a vertical promo video with React and Remotion using animations.

Introduction

30 Second Summary

Short-form video dominates social media. Every day, millions of vertical videos fight for attention on TikTok, Instagram Reels, and YouTube Shorts.

In this project, you will build an animated social media promo video entirely with code using React and Remotion. You will create animated scenes, sequence them together, and render the final result as an MP4 file ready to upload.

What You'll Build

A vertical (1080x1920) promo video with three animated scenes, built with TypeScript and rendered as an MP4 using Remotion.

By the end of this project, you'll have:

  • A Remotion project with a custom 9:16 vertical composition for social media.
  • Three animated scene components (Title, Tips, and CTA) using spring physics and interpolation.
  • A rendered MP4 file ready to upload to TikTok or Instagram Reels.
  • Secret Mission: Add smooth crossfade transitions between scenes for a polished, professional look.

Are there any prerequisites?

This project assumes you are familiar with React components and TypeScript basics. No prior video editing or Remotion experience is needed.

Set Up Node.js and Scaffold a Remotion Project

To build a video with code, you need a JavaScript runtime that can execute React components outside a browser. Node.js is that runtime, and it comes with `npm` for managing packages.

Remotion is the framework that turns React components into real video files. In this step, you'll install Node.js, scaffold a Remotion project, and launch Remotion Studio to preview your work live.

In this step, get ready to:
  • Install and verify Node.js.
  • Scaffold a new Remotion project using the Hello World template.
  • Start Remotion Studio and explore the project structure.
Check if Node.js is installed

Remotion requires Node.js version 16 or higher. Let's check whether you already have it installed.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check your Node.js version by running:
node --version

✔️ I see a version number

Your Node.js version should start with v16 or higher (for example, v20.11.0). If it does, you're all set. `npm` is bundled with Node.js, so both are ready to go.

ⓧ Command not found

No worries. You just need to install Node.js first.

  • Visit nodejs.org in your browser.
  • Download the LTS version (this includes npm).
  • Open the downloaded file and follow the installer prompts.
  • Once the installation finishes, close and reopen your terminal.
  • Run node --version again to confirm it's working.

Still seeing 'command not found' after installing?

Make sure you closed and reopened your terminal after installation. The terminal needs a fresh session to pick up the new node command.

Help me troubleshoot my Node.js installation on macOS.

Scaffold a new Remotion project

Remotion provides a scaffold command that sets up a complete project with all the dependencies and configuration you need. You'll use the Hello World template, which includes a working example animation to help you understand the project structure before modifying it.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Scaffold your Remotion project by running the command below.

This command starts an interactive wizard. It will prompt you for a template choice and a project name.

npx create-video@latest
  • If prompted to install create-video, type y and press Enter to confirm.
  • Select the Hello World template when prompted.
  • Enter social-promo as the project name.

Why the Hello World template?

The Hello World template is Remotion's recommended starting point. It includes a working example composition with animations, so you can see what a Remotion project looks like before you start building your own scenes.

You'll replace this example code in the next step. Seeing it first helps you understand the project structure.

The scaffold installs all dependencies automatically. Once it finishes, you'll see a success message in your terminal.

  • Move into your new project folder by running:
cd social-promo
  • Confirm the project files exist by running:
ls

You should see folders like src/ and public/, along with files like package.json and tsconfig.json.

Scaffold command failed?

Make sure your Node.js version is 16 or higher by running node --version.

If you see a network error, check your internet connection and try the command again.

Help me fix the Remotion scaffold command.

Now open the project in VS Code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the social-promo folder, then click Open.

VS Code opens with your project. You should see a file tree in the left sidebar with folders like src/ and public/.

Start Remotion Studio and explore the project

Remotion Studio is a browser-based preview tool that shows your video in real time as you code. You can scrub through frames, play the video, and see changes instantly.

  • Press Ctrl+` (backtick key, above Tab) to open VS Code's integrated terminal.
  • Start Remotion Studio by running:
npm run dev

Your browser opens automatically with Remotion Studio. You should see a live preview of the Hello World animation with playback controls at the bottom of the screen.

What am I looking at?

Remotion Studio has three main areas:

  • The main preview shows a live render of your video composition.
  • The timeline at the bottom lets you scrub through individual frames.
  • The sidebar on the left lists all registered compositions in your project.
  • Click the play button in the timeline to watch the Hello World animation.
  • Drag the playhead along the timeline to scrub through frames manually.

Now take a look at how the project is organized in VS Code.

  • Switch back to VS Code and expand the src/ folder in the Explorer sidebar.

Key files in your Remotion project

  • src/Root.tsx is where compositions are registered. Think of it as the table of contents for your video project.
  • src/index.ts is the entry point that tells Remotion where to find your Root component.
  • Individual component files in src/ define what each video scene looks like. You'll create your own scene components starting in the next step.

Remotion Studio didn't open?

Make sure you're running npm run dev from inside the social-promo folder. If your terminal is in a different directory, run cd ~/Desktop/social-promo first.

If you see a "port already in use" error, close any other development servers running on your machine and try again.

Help me get Remotion Studio running.

Your environment is ready and Remotion Studio is running. Next up, you'll replace the Hello World code with a custom 9:16 composition and build your first animated title scene.

Create the Promo Video Composition and Title Scene

Your Remotion project is scaffolded and Studio is live in your browser. Now it's time to replace the default Hello World content with your own video.

Every Remotion video starts with a composition. A composition defines the video's dimensions, frame rate, and duration. You'll set up a vertical 9:16 composition sized for social media, then build your first animated scene: a title card with a physics-based bounce effect.

In this step, get ready to:
  • Register a 1080x1920 vertical composition in Root.tsx.
  • Build a Title scene with spring-bounce and fade-in animations.
  • Connect the Title scene to the composition using a Sequence.
Set up the 9:16 composition

Social media platforms like TikTok and Instagram Reels use a 9:16 vertical aspect ratio. The Composition component in Remotion is where you define these dimensions along with the frame rate and total duration.

  • Open src/Root.tsx in VS Code.
  • Select all the existing code and replace it with the following:
import { Composition } from "remotion";
import { SocialPromo } from "./SocialPromo";

export const RemotionRoot: React.FC = () => {
  return (
    <Composition
      // Unique ID used by Studio and the render command
      id="SocialPromo"
      component={SocialPromo}
      // 270 frames at 30fps = 9 seconds total
      durationInFrames={270}
      fps={30}
      // 9:16 vertical aspect ratio for social media
      width={1080}
      height={1920}
    />
  );
};

What does this code do?

  • id="SocialPromo" gives your composition a name that appears in the Studio sidebar and is used when rendering.
  • durationInFrames={270} at fps={30} means the video is 9 seconds long (270 divided by 30).
  • width={1080} and height={1920} set the vertical 9:16 dimensions used by TikTok and Instagram Reels.
  • component={SocialPromo} points to the React component that defines what the video looks like. You'll create this component shortly.
  • Save Root.tsx.

Seeing an error in Remotion Studio?

Your Studio will show an error because the SocialPromo component doesn't exist yet. This is expected. You'll create it in a few moments.

✔️ Awesome, I've got everything!

Great. Make sure Root.tsx is saved before moving on.

ⓧ I'd like to double check the full code

Here's the complete Root.tsx file for comparison.

import { Composition } from "remotion";
import { SocialPromo } from "./SocialPromo";

export const RemotionRoot: React.FC = () => {
  return (
    <Composition
      id="SocialPromo"
      component={SocialPromo}
      durationInFrames={270}
      fps={30}
      width={1080}
      height={1920}
    />
  );
};
Build the Title scene component

The Title scene is the opening card of your video. It uses two of Remotion's core animation tools: spring() for a physics-based bounce and interpolate() for smooth fade-in transitions.

  • Create a new file called src/Title.tsx inside your social-promo project.
  • Add the following code to Title.tsx:
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

export const Title: React.FC = () => {
  // Get the current frame number and frames-per-second
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Spring animation drives a bounce-in scale effect on the title
  const scale = spring({
    frame,
    fps,
    config: {
      damping: 12,
      stiffness: 200,
      mass: 0.8,
    },
  });

  // Fade the subtitle in between frames 20 and 40
  const subtitleOpacity = interpolate(frame, [20, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Slide the subtitle upward from 20px below to its final position
  const subtitleY = interpolate(frame, [20, 40], [20, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      {/* Title text with spring-animated scale */}
      <div
        style={{
          transform: `scale(${scale})`,
          color: "white",
          fontSize: 80,
          fontWeight: "bold",
          fontFamily: "Arial, sans-serif",
          textAlign: "center",
          lineHeight: 1.2,
        }}
      >
        Level Up Your Code
      </div>
      {/* Subtitle with fade and slide animation */}
      <div
        style={{
          opacity: subtitleOpacity,
          transform: `translateY(${subtitleY}px)`,
          color: "rgba(255, 255, 255, 0.9)",
          fontSize: 36,
          fontFamily: "Arial, sans-serif",
          marginTop: 30,
          textAlign: "center",
        }}
      >
        3 tips you need to know
      </div>
    </AbsoluteFill>
  );
};

What does this code do?

  • useCurrentFrame() returns the current frame number. Every time Remotion advances a frame, this value changes and the component re-renders with updated animation values.
  • spring() creates a physics-based animation that goes from 0 to 1. The config (damping: 12, stiffness: 200, mass: 0.8) controls how snappy the bounce is. This value is applied to the title's transform: scale() so it pops into view.
  • interpolate() maps a frame number to an output value. For the subtitle, frames 20 through 40 map to opacity 0 through 1, creating a fade-in that starts slightly after the title bounce.
  • extrapolateLeft: "clamp" and extrapolateRight: "clamp" prevent the output from going outside the 0-to-1 range. Without clamping, Remotion would extrapolate values beyond the range, causing unexpected behavior.
  • AbsoluteFill fills the entire composition frame, acting as the scene's canvas.
  • Save Title.tsx.

You won't see a change in Studio yet. The next chunk creates the SocialPromo component that uses this Title scene.

✔️ Awesome, I've got everything!

Great. Make sure Title.tsx is saved before moving on.

ⓧ I'd like to double check the full code

Here's the complete Title.tsx file for comparison.

import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

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

  const scale = spring({
    frame,
    fps,
    config: {
      damping: 12,
      stiffness: 200,
      mass: 0.8,
    },
  });

  const subtitleOpacity = interpolate(frame, [20, 40], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  const subtitleY = interpolate(frame, [20, 40], [20, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      <div
        style={{
          transform: `scale(${scale})`,
          color: "white",
          fontSize: 80,
          fontWeight: "bold",
          fontFamily: "Arial, sans-serif",
          textAlign: "center",
          lineHeight: 1.2,
        }}
      >
        Level Up Your Code
      </div>
      <div
        style={{
          opacity: subtitleOpacity,
          transform: `translateY(${subtitleY}px)`,
          color: "rgba(255, 255, 255, 0.9)",
          fontSize: 36,
          fontFamily: "Arial, sans-serif",
          marginTop: 30,
          textAlign: "center",
        }}
      >
        3 tips you need to know
      </div>
    </AbsoluteFill>
  );
};
Wire the Title into the SocialPromo composition

Remotion's Sequence component controls when each scene appears in the timeline. It shifts the frame counter for its children so each scene's animation starts from frame 0, regardless of where it sits in the overall video.

  • Create a new file called src/SocialPromo.tsx inside your social-promo project.
  • Add the following code to SocialPromo.tsx:
import { AbsoluteFill, Sequence } from "remotion";
import { Title } from "./Title";

export const SocialPromo: React.FC = () => {
  return (
    <AbsoluteFill>
      {/* Title scene plays for the first 90 frames (3 seconds at 30fps) */}
      <Sequence durationInFrames={90}>
        <Title />
      </Sequence>
    </AbsoluteFill>
  );
};

What does this code do?

  • Sequence with durationInFrames={90} gives the Title scene 90 frames, which is 3 seconds at 30fps.
  • Inside the Sequence, useCurrentFrame() returns frame 0 on the first frame of the Sequence, not the global frame number. This means the Title's spring animation always starts from the beginning.
  • AbsoluteFill wraps all sequences, layering them on top of each other. Right now there's only one scene. You'll add more in the next step.
  • Save SocialPromo.tsx.
  • Check Remotion Studio in your browser. You should see SocialPromo listed in the compositions sidebar on the left.
  • Click play or scrub through the timeline. The title "Level Up Your Code" bounces in with a spring animation, followed by the subtitle "3 tips you need to know" fading in shortly after.

Don't see the animation in Studio?

  • Make sure all three files are saved: Root.tsx, Title.tsx, and SocialPromo.tsx.
  • Check that the import paths match exactly. ./SocialPromo in Root.tsx and ./Title in SocialPromo.tsx must point to the correct files.
  • If Studio shows a blank screen, open the browser console for red error messages. A missing import or typo in a file name is the most common cause.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure all three files are saved before moving on.

ⓧ I'd like to double check the full code

Here's the complete SocialPromo.tsx file for comparison.

import { AbsoluteFill, Sequence } from "remotion";
import { Title } from "./Title";

export const SocialPromo: React.FC = () => {
  return (
    <AbsoluteFill>
      <Sequence durationInFrames={90}>
        <Title />
      </Sequence>
    </AbsoluteFill>
  );
};

Your Title scene is animated and playing in the Studio preview. Next up, you'll build the Tips scene with staggered text reveals that bring each tip to life one after another.

Build the Tips Scene with Staggered Animations

Your Title scene bounces into view with a spring animation. Now the video needs its core content: three coding tips that appear one after another in a polished sequence.

This staggered reveal effect makes each tip feel intentional instead of dumping everything on screen at once. You will use Remotion's interpolate() function to control the timing of each tip's entrance, offsetting each one by a few frames.

In this step, get ready to:
  • Create a Tips scene component with staggered fade-in animations.
  • Style each tip as a rounded, frosted-glass card with a numbered label.
  • Sequence the Tips scene after the Title in your composition.
Create the Tips scene component

The Tips scene displays three coding tips that slide in one after another. Each tip starts its animation 20 frames after the previous one, creating a cascading reveal.

  • Create a new file called Tips.tsx inside your src/ folder.
  • Add the imports and tips data array by pasting this code:
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";

// The three tips to display in the scene
const tips = [
  { number: "01", text: "Write code every single day" },
  { number: "02", text: "Read other people's code" },
  { number: "03", text: "Build projects, not tutorials" },
];

What does this code do?

  • The imports pull in AbsoluteFill for full-frame positioning, interpolate for animation curves, and useCurrentFrame to track which frame is rendering.
  • The tips array lives outside the component so it is defined once and reused every render.
  • Each tip has a number for its label and text for the actual content.
Style the tips with consistent visual design

Now you need the component that renders these tips as styled cards with staggered animations. Each tip fades in and slides up, offset by 20 frames from the previous one.

  • In src/Tips.tsx, add the Tips component below the tips array by pasting this code:
export const Tips: React.FC = () => {
  const frame = useCurrentFrame();

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        // Same gradient as the Title scene for visual consistency
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          gap: 30,
          width: "100%",
        }}
      >
        {tips.map((tip, index) => {
          // Each tip starts animating 20 frames after the previous one
          const delay = index * 20;

          // Fade in over 15 frames, starting at this tip's delay
          const opacity = interpolate(frame, [delay, delay + 15], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });

          // Slide up 30px over the same 15 frames
          const translateY = interpolate(frame, [delay, delay + 15], [30, 0], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });

          return (
            <div
              key={tip.number}
              style={{
                opacity,
                transform: `translateY(${translateY}px)`,
                // Semi-transparent frosted card
                background: "rgba(255, 255, 255, 0.15)",
                borderRadius: 20,
                padding: "30px 40px",
                display: "flex",
                alignItems: "center",
                gap: 24,
              }}
            >
              {/* Numbered label */}
              <div
                style={{
                  color: "rgba(255, 255, 255, 0.5)",
                  fontSize: 48,
                  fontWeight: "bold",
                  fontFamily: "Arial, sans-serif",
                  minWidth: 80,
                }}
              >
                {tip.number}
              </div>
              {/* Tip text */}
              <div
                style={{
                  color: "white",
                  fontSize: 40,
                  fontWeight: "bold",
                  fontFamily: "Arial, sans-serif",
                  lineHeight: 1.3,
                }}
              >
                {tip.text}
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};

What does this code do?

  • useCurrentFrame() gives the current frame number. Because this component will live inside a Sequence, the frame resets to 0 at the start of the Tips scene.
  • delay = index * 20 creates the stagger. Tip 1 starts at frame 0, tip 2 at frame 20, tip 3 at frame 40.
  • Each interpolate() call maps a 15-frame window to an animation range. extrapolateLeft: "clamp" and extrapolateRight: "clamp" prevent values from going outside the output range.
  • The gradient background matches the Title scene so the scenes look like one continuous video.
  • Each card uses rgba(255, 255, 255, 0.15) for a frosted-glass look on top of the purple gradient. The numbered labels sit at half-opacity to create a visual hierarchy.
  • Save Tips.tsx.

You will not see the Tips scene in Remotion Studio yet. You need to wire it into your composition first.

Seeing a red error in VS Code?

  • Check that the file is named exactly Tips.tsx (capital T, .tsx extension) and that it lives directly inside src/.
  • Make sure the component function is placed below the tips array, not above it. The function references the array, so it must come after.
  • help me fix a TypeScript error in my Tips.tsx file

✔️ Awesome, I've got everything!

Make sure you have saved Tips.tsx before moving on.

ⓧ I'd like to double check the full code

import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";

const tips = [
  { number: "01", text: "Write code every single day" },
  { number: "02", text: "Read other people's code" },
  { number: "03", text: "Build projects, not tutorials" },
];

export const Tips: React.FC = () => {
  const frame = useCurrentFrame();

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          gap: 30,
          width: "100%",
        }}
      >
        {tips.map((tip, index) => {
          const delay = index * 20;
          const opacity = interpolate(frame, [delay, delay + 15], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });
          const translateY = interpolate(frame, [delay, delay + 15], [30, 0], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });

          return (
            <div
              key={tip.number}
              style={{
                opacity,
                transform: `translateY(${translateY}px)`,
                background: "rgba(255, 255, 255, 0.15)",
                borderRadius: 20,
                padding: "30px 40px",
                display: "flex",
                alignItems: "center",
                gap: 24,
              }}
            >
              <div
                style={{
                  color: "rgba(255, 255, 255, 0.5)",
                  fontSize: 48,
                  fontWeight: "bold",
                  fontFamily: "Arial, sans-serif",
                  minWidth: 80,
                }}
              >
                {tip.number}
              </div>
              <div
                style={{
                  color: "white",
                  fontSize: 40,
                  fontWeight: "bold",
                  fontFamily: "Arial, sans-serif",
                  lineHeight: 1.3,
                }}
              >
                {tip.text}
              </div>
            </div>
          );
        })}
      </div>
    </AbsoluteFill>
  );
};
Add the Tips scene to SocialPromo

The Tips component is ready, but it is not part of your video yet. You need to add it as a second Sequence in your composition so it plays right after the Title scene ends at frame 90.

  • Open src/SocialPromo.tsx and find the existing Title import at the top of the file:
import { Title } from "./Title";
  • Add a new import for Tips directly below it:
import { Tips } from "./Tips";
  • Find the closing </Sequence> tag for the Title. Add the Tips Sequence below it by pasting this code:
      <Sequence from={90} durationInFrames={120}>
        <Tips />
      </Sequence>

What does this Sequence do?

  • from={90} tells Remotion to start this scene at frame 90 (the 3-second mark at 30 fps). This is exactly where the Title scene ends.
  • durationInFrames={120} gives the Tips scene 4 seconds of screen time (120 frames at 30 fps).
  • Inside the Sequence, useCurrentFrame() resets to 0. So the Tips component's stagger starts from frame 0 of its own timeline, not from frame 90 of the global timeline.
  • Save SocialPromo.tsx.
  • Switch to Remotion Studio in your browser. Scrub through the timeline past the 3-second mark. You should see the Title scene play first, then three tip cards sliding in one after another on the same purple gradient background.

Tips not showing up after the Title?

  • Make sure the Tips Sequence is placed after the Title Sequence inside the <AbsoluteFill> wrapper, not outside it.
  • Confirm the import path says ./Tips (with a capital T). File name casing matters.
  • Check that from={90} is set on the Tips Sequence. Without it, both scenes start at frame 0 and overlap.
  • help me debug why my Tips scene is not appearing in Remotion Studio

✔️ Awesome, I've got everything!

Make sure you have saved SocialPromo.tsx before moving on.

ⓧ I'd like to double check the full code

import { AbsoluteFill, Sequence } from "remotion";
import { Title } from "./Title";
import { Tips } from "./Tips";

export const SocialPromo: React.FC = () => {
  return (
    <AbsoluteFill>
      <Sequence durationInFrames={90}>
        <Title />
      </Sequence>
      <Sequence from={90} durationInFrames={120}>
        <Tips />
      </Sequence>
    </AbsoluteFill>
  );
};

Your video now has a title and a tips section that plays in sequence. Next up, you will build a call-to-action outro with a pulsing animation to close out the video.

Add a Call-to-Action Outro and Render the Video

Your video has a punchy title intro and three staggered tips. Every good promo video needs a strong closing scene that tells viewers what to do next.

In this step, you'll build an animated call-to-action (CTA) scene with a pulsing button effect, sequence all three scenes into the final composition, and render the whole thing as an MP4 file.

In this step, get ready to:
  • Build a CTA scene with spring animation and a looping pulse effect.
  • Complete the video composition by sequencing all three scenes together.
  • Render the final 9-second video as an MP4 file.
Build the CTA scene component

The CTA scene is the final thing viewers see. It needs to grab attention, so you'll combine two animation techniques: a spring() scale-in for the entrance and a continuously looping pulse on the button powered by Math.sin().

  • Create a new file called CTA.tsx inside the src/ folder of your social-promo project.
  • Paste the following code into src/CTA.tsx:
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

export const CTA: React.FC = () => {
  // Get the current frame number and fps for animation calculations
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  // Spring animation for the scale-in entrance effect
  const scale = spring({
    frame,
    fps,
    config: {
      damping: 10,
      stiffness: 150,
      mass: 0.6,
    },
  });

  // Looping pulse: Math.sin() oscillates between -1 and 1, mapped to a subtle 1 to 1.05 scale
  const pulse = interpolate(Math.sin(frame * 0.15), [-1, 1], [1, 1.05]);

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      {/* Outer container scales in with the spring */}
      <div
        style={{
          transform: `scale(${scale})`,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 40,
        }}
      >
        <div
          style={{
            color: "white",
            fontSize: 64,
            fontWeight: "bold",
            fontFamily: "Arial, sans-serif",
            textAlign: "center",
          }}
        >
          Follow for more!
        </div>
        {/* Button pulses continuously to draw the viewer's eye */}
        <div
          style={{
            transform: `scale(${pulse})`,
            background: "white",
            color: "#764ba2",
            fontSize: 36,
            fontWeight: "bold",
            fontFamily: "Arial, sans-serif",
            padding: "20px 60px",
            borderRadius: 50,
          }}
        >
          @yourhandle
        </div>
      </div>
    </AbsoluteFill>
  );
};

What does this code do?

This component layers two different animation strategies on top of each other.

  • scale uses spring() to animate the whole container from 0 to 1 with a bouncy entrance. This runs once when the scene starts.
  • pulse feeds Math.sin(frame * 0.15) into interpolate(). Since Math.sin() oscillates between -1 and 1 forever, the button gently scales between 1 and 1.05 on a continuous loop.
  • The 0.15 multiplier controls how fast the pulse cycles. A smaller number means a slower, more subtle pulse.
  • Save src/CTA.tsx.

You won't see anything new in Remotion Studio yet. The next substep wires the CTA into your video composition.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/CTA.tsx file for comparison.

import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";

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

  const scale = spring({
    frame,
    fps,
    config: {
      damping: 10,
      stiffness: 150,
      mass: 0.6,
    },
  });

  const pulse = interpolate(Math.sin(frame * 0.15), [-1, 1], [1, 1.05]);

  return (
    <AbsoluteFill
      style={{
        justifyContent: "center",
        alignItems: "center",
        background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
        padding: 60,
      }}
    >
      <div
        style={{
          transform: `scale(${scale})`,
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 40,
        }}
      >
        <div
          style={{
            color: "white",
            fontSize: 64,
            fontWeight: "bold",
            fontFamily: "Arial, sans-serif",
            textAlign: "center",
          }}
        >
          Follow for more!
        </div>
        <div
          style={{
            transform: `scale(${pulse})`,
            background: "white",
            color: "#764ba2",
            fontSize: 36,
            fontWeight: "bold",
            fontFamily: "Arial, sans-serif",
            padding: "20px 60px",
            borderRadius: 50,
          }}
        >
          @yourhandle
        </div>
      </div>
    </AbsoluteFill>
  );
};
Wire the CTA into your composition

The CTA component exists but your video doesn't know about it yet. You need to import it and place it inside a Sequence that starts right after the Tips scene ends.

  • Open src/SocialPromo.tsx.
  • Find this import line near the top of the file:
import { Tips } from "./Tips";
  • Add a new import for the CTA component directly below it:
import { CTA } from "./CTA";
  • Now scroll down to the JSX. Find the closing </Sequence> tag for the Tips scene:
      <Sequence from={90} durationInFrames={120}>
        <Tips />
      </Sequence>
  • Add the CTA as a third Sequence directly below the Tips Sequence by pasting this code:
      <Sequence from={210} durationInFrames={60}>
        <CTA />
      </Sequence>

Why from={210} and durationInFrames={60}?

The Title runs for 90 frames (0 to 89). Tips starts at frame 90 and runs for 120 frames (90 to 209). That means the CTA picks up right at frame 210.

60 frames at 30 fps gives the CTA exactly 2 seconds. Add all three scenes together: 90 + 120 + 60 = 270 frames, which matches the total durationInFrames you set on the composition in Root.tsx.

  • Save src/SocialPromo.tsx.
  • Switch to Remotion Studio in your browser.
  • Scrub the timeline from start to finish. You should see three scenes play in order: the Title bouncing in (0 to 3 seconds), the Tips staggering in (3 to 7 seconds), and the CTA scaling in with a pulsing button (7 to 9 seconds).

CTA scene not showing up?

  • Make sure you saved both src/CTA.tsx and src/SocialPromo.tsx. Remotion Studio hot-reloads on save, but only if both files are saved.
  • Check that the import path is ./CTA (not ./cta or ./CTA.tsx). The filename is case-sensitive.
  • Verify that from={210} is set on the CTA Sequence. Without it, the CTA would overlap with the Title scene at frame 0.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete src/SocialPromo.tsx file for comparison.

import { AbsoluteFill, Sequence } from "remotion";
import { Title } from "./Title";
import { Tips } from "./Tips";
import { CTA } from "./CTA";

export const SocialPromo: React.FC = () => {
  return (
    <AbsoluteFill>
      <Sequence durationInFrames={90}>
        <Title />
      </Sequence>
      <Sequence from={90} durationInFrames={120}>
        <Tips />
      </Sequence>
      <Sequence from={210} durationInFrames={60}>
        <CTA />
      </Sequence>
    </AbsoluteFill>
  );
};
Render the final MP4

Remotion Studio is great for previewing, but the whole point of this project is to produce a real video file. Remotion's CLI renders every frame and encodes them into an MP4 using a bundled copy of FFmpeg.

  • Open a new terminal in VS Code by pressing Ctrl+` (macOS and Windows). If Remotion Studio is running in your current terminal, open a second terminal tab.
  • Make sure you are inside the social-promo project folder.
  • Render the video by running this command:
npx remotion render SocialPromo out/social-promo.mp4

What does this command do?

  • SocialPromo is the composition ID you set in Root.tsx. Remotion uses this to find the right composition to render.
  • out/social-promo.mp4 is the output path. Remotion creates the out/ folder automatically if it doesn't exist.
  • Remotion renders each of the 270 frames individually, then encodes them into an MP4 file. This takes a few seconds to a minute depending on your machine.

You should see a progress bar in the terminal as Remotion renders each frame. When it finishes, you'll see a message confirming the output file path.

  • Open the out/ folder inside your social-promo project and double-click social-promo.mp4 to play it in your default video player.
  • You should see a 9-second vertical video with your title bouncing in, three tips appearing one by one, and the CTA pulsing at the end.

Render failing or hanging?

  • If you see a "composition not found" error, check that the ID in the render command (SocialPromo) matches the id prop on your Composition in Root.tsx. The casing must be exact.
  • Make sure you are running the command from inside the social-promo folder, not a parent directory.
  • If the render starts but crashes mid-way, check for any TypeScript errors in your component files. A typo in a style property or a missing import can cause render failures.

Need help debugging the render?

You just created a complete social media promo video with nothing but React and TypeScript. The hard cuts between scenes get the job done, but what if you could blend them together with smooth crossfades? That's exactly what the Secret Mission covers.

Secret mission

Add Crossfade Transitions Between Scenes

Right now your three scenes cut abruptly from one to the next. Real social media videos use smooth transitions to keep viewers watching. Can you add crossfade transitions so each scene blends into the next?

Before you go

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your project files for future video creation, or delete them to free up disk space.

Resources you used:

  • Node.js installation (installed or already present on your machine).
  • Remotion project folder social-promo on your Desktop.
  • Rendered MP4 file at out/social-promo.mp4 inside the project folder.
  • Remotion Studio local dev server (runs only while npm run dev is active).

Keep everything running

No action needed. Choose this if you plan to keep building on your Remotion project or want to create more videos.

  • Your social-promo folder stays on your Desktop, ready for you to open anytime.
  • Run npm run dev from the project folder whenever you want to preview videos in Remotion Studio.
  • Node.js stays installed and available for other projects too.

Pause - I'll come back to this later

Stop the dev server to free up memory. Your project files stay intact so you can pick up where you left off.

  • If Remotion Studio is still running in your terminal, press Ctrl+C to stop it.
  • Your social-promo folder and rendered MP4 remain on disk.
  • When you come back, open the folder in VS Code and run npm run dev to start Remotion Studio again.

Delete - I don't want to use this again

Remove all project files and free up disk space. You can always re-scaffold a new Remotion project later.

  • If Remotion Studio is still running in your terminal, press Ctrl+C to stop it.
  • Delete the entire social-promo folder from your Desktop.
  • Keep Node.js installed if you use it for other projects. If you only installed it for this project and want to remove it, uninstall it from Settings (Windows) or by removing it from /usr/local/bin/node (macOS).

Mission accomplished

That's a Wrap!

Nice work! You just built a social media promo video entirely with code. No video editor, no drag-and-drop timeline. Just React, TypeScript, and Remotion.

You've learned how to:

  • Built a Remotion project from scratch with a custom 9:16 vertical composition sized for TikTok and Instagram Reels.
  • Created three animated scene components using useCurrentFrame(), interpolate(), and spring() for smooth, physics-based animations.
  • Sequenced multiple scenes with Sequence components and rendered the final video as an MP4 file ready to upload.
  • Added crossfade transitions between scenes using opacity-based interpolation for a polished, professional look.

Ready to quiz yourself?