Skip to main content

Remotion vs Motion Canvas vs Revideo: Programmatic Video (2026)

·PkgPulse Team

TL;DR

Remotion is the React framework for programmatic video — write videos as React components, render to MP4/WebM, server-side rendering, Player component, GitHub Actions rendering. Motion Canvas is the code-driven animation framework — TypeScript generator functions, tweening, scene graphs, real-time editor, designed for explanatory videos and visualizations. Revideo is the open-source fork of Motion Canvas — adds rendering API, server-side rendering, template system, designed for automated video production pipelines. In 2026: Remotion for React developers making videos, Motion Canvas for hand-crafted animations, Revideo for automated video pipelines.

Key Takeaways

  • Remotion: remotion ~60K weekly downloads — React components as video, MP4 rendering
  • Motion Canvas: @motion-canvas/core ~8K weekly downloads — generator-based animations
  • Revideo: @revideo/core ~3K weekly downloads — Motion Canvas fork, rendering API
  • Remotion leverages the entire React ecosystem for video creation
  • Motion Canvas provides the finest animation control with generator functions
  • Revideo adds server-side rendering and template APIs to Motion Canvas

Remotion

Remotion — React for video:

Setup

npx create-video@latest my-video
cd my-video
npm start

Basic composition

// src/PackageStats.tsx
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, spring } from "remotion"

export const PackageStats: React.FC<{
  packageName: string
  downloads: number
  version: string
}> = ({ packageName, downloads, version }) => {
  const frame = useCurrentFrame()
  const { fps } = useVideoConfig()

  // Animate title entry:
  const titleOpacity = interpolate(frame, [0, 30], [0, 1], {
    extrapolateRight: "clamp",
  })
  const titleY = spring({ frame, fps, from: -50, to: 0, durationInFrames: 30 })

  // Animate download counter:
  const counterValue = interpolate(frame, [30, 90], [0, downloads], {
    extrapolateRight: "clamp",
  })

  // Animate version badge:
  const badgeScale = spring({ frame: frame - 60, fps, from: 0, to: 1 })

  return (
    <AbsoluteFill
      style={{
        background: "linear-gradient(135deg, #0f172a, #1e293b)",
        justifyContent: "center",
        alignItems: "center",
        fontFamily: "Inter, sans-serif",
      }}
    >
      <div
        style={{
          opacity: titleOpacity,
          transform: `translateY(${titleY}px)`,
          fontSize: 72,
          fontWeight: 800,
          color: "white",
          marginBottom: 40,
        }}
      >
        {packageName}
      </div>

      <div style={{ fontSize: 48, color: "#94a3b8", marginBottom: 20 }}>
        Weekly Downloads
      </div>

      <div
        style={{
          fontSize: 96,
          fontWeight: 900,
          color: "#3b82f6",
          fontVariantNumeric: "tabular-nums",
        }}
      >
        {Math.floor(counterValue).toLocaleString()}
      </div>

      <div
        style={{
          marginTop: 40,
          transform: `scale(${badgeScale})`,
          background: "#22c55e",
          padding: "12px 32px",
          borderRadius: 40,
          fontSize: 32,
          fontWeight: 600,
          color: "white",
        }}
      >
        v{version}
      </div>
    </AbsoluteFill>
  )
}

Composition setup

// src/Root.tsx
import { Composition, Series } from "remotion"
import { PackageStats } from "./PackageStats"
import { ComparisonTable } from "./ComparisonTable"
import { TrendChart } from "./TrendChart"

export const RemotionRoot: React.FC = () => {
  return (
    <>
      {/* Single scene: */}
      <Composition
        id="PackageStats"
        component={PackageStats}
        durationInFrames={150}
        fps={30}
        width={1920}
        height={1080}
        defaultProps={{
          packageName: "React",
          downloads: 25000000,
          version: "19.0.0",
        }}
      />

      {/* Multi-scene video: */}
      <Composition
        id="PackageComparison"
        component={PackageComparisonVideo}
        durationInFrames={450}
        fps={30}
        width={1920}
        height={1080}
      />
    </>
  )
}

// Multi-scene with Series:
const PackageComparisonVideo: React.FC = () => {
  return (
    <Series>
      <Series.Sequence durationInFrames={150}>
        <PackageStats packageName="React" downloads={25000000} version="19.0.0" />
      </Series.Sequence>
      <Series.Sequence durationInFrames={150}>
        <PackageStats packageName="Vue" downloads={4500000} version="3.5.0" />
      </Series.Sequence>
      <Series.Sequence durationInFrames={150}>
        <ComparisonTable
          packages={[
            { name: "React", downloads: 25000000 },
            { name: "Vue", downloads: 4500000 },
          ]}
        />
      </Series.Sequence>
    </Series>
  )
}

Rendering

# Render to MP4:
npx remotion render PackageStats out/package-stats.mp4

# Render with props:
npx remotion render PackageStats out/react-stats.mp4 \
  --props='{"packageName":"React","downloads":25000000,"version":"19.0.0"}'

# Render as GIF:
npx remotion render PackageStats out/stats.gif --image-format=png

# Server-side rendering with Lambda:
npx remotion lambda render PackageStats \
  --props='{"packageName":"React","downloads":25000000}'
// Programmatic rendering:
import { bundle } from "@remotion/bundler"
import { renderMedia, getCompositions } from "@remotion/renderer"

async function render(props: Record<string, any>) {
  const bundled = await bundle({ entryPoint: "./src/index.ts" })

  const compositions = await getCompositions(bundled)
  const composition = compositions.find((c) => c.id === "PackageStats")!

  await renderMedia({
    composition,
    serveUrl: bundled,
    codec: "h264",
    outputLocation: `out/${props.packageName}-stats.mp4`,
    inputProps: props,
  })
}

// Batch render:
const packages = [
  { packageName: "React", downloads: 25000000, version: "19.0.0" },
  { packageName: "Vue", downloads: 4500000, version: "3.5.0" },
  { packageName: "Svelte", downloads: 1200000, version: "5.0.0" },
]

for (const props of packages) {
  await render(props)
}

Player component (embed in web apps)

import { Player } from "@remotion/player"
import { PackageStats } from "./PackageStats"

function VideoPreview({ pkg }: { pkg: Package }) {
  return (
    <Player
      component={PackageStats}
      inputProps={{
        packageName: pkg.name,
        downloads: pkg.downloads,
        version: pkg.version,
      }}
      durationInFrames={150}
      compositionWidth={1920}
      compositionHeight={1080}
      fps={30}
      style={{ width: "100%", aspectRatio: "16/9" }}
      controls
      autoPlay
      loop
    />
  )
}

Motion Canvas

Motion Canvas — code-driven animations:

Setup

npm create @motion-canvas@latest my-animation
cd my-animation
npm start

Scene with generator functions

// src/scenes/packageStats.tsx
import { makeScene2D, Txt, Rect, Circle, Layout } from "@motion-canvas/2d"
import { createRef, all, waitFor, chain, loop } from "@motion-canvas/core"

export default makeScene2D(function* (view) {
  const title = createRef<Txt>()
  const counter = createRef<Txt>()
  const badge = createRef<Rect>()
  const container = createRef<Layout>()

  // Build the scene:
  view.add(
    <Layout ref={container} layout direction="column" alignItems="center" gap={40}>
      <Txt
        ref={title}
        text="React"
        fontSize={72}
        fontWeight={800}
        fill="#ffffff"
        opacity={0}
        y={-50}
      />
      <Txt
        text="Weekly Downloads"
        fontSize={36}
        fill="#94a3b8"
      />
      <Txt
        ref={counter}
        text="0"
        fontSize={96}
        fontWeight={900}
        fill="#3b82f6"
      />
      <Rect
        ref={badge}
        fill="#22c55e"
        radius={40}
        padding={[12, 32]}
        scale={0}
      >
        <Txt text="v19.0.0" fontSize={32} fontWeight={600} fill="#ffffff" />
      </Rect>
    </Layout>
  )

  // Set background:
  view.fill("#0f172a")

  // Animate title (fade in + slide up):
  yield* all(
    title().opacity(1, 0.5),
    title().y(0, 0.5),
  )

  // Animate counter (count up):
  yield* counter().text("25,000,000", 2)

  // Animate badge (scale in):
  yield* badge().scale(1, 0.3)

  // Hold:
  yield* waitFor(1)
})

Complex animation sequences

// src/scenes/comparison.tsx
import { makeScene2D, Txt, Rect, Line } from "@motion-canvas/2d"
import { createRef, all, sequence, waitFor, easeOutCubic } from "@motion-canvas/core"

export default makeScene2D(function* (view) {
  view.fill("#0f172a")

  const packages = [
    { name: "React", downloads: 25000000, color: "#61dafb" },
    { name: "Vue", downloads: 4500000, color: "#42b883" },
    { name: "Svelte", downloads: 1200000, color: "#ff3e00" },
  ]

  const maxDownloads = Math.max(...packages.map((p) => p.downloads))
  const barRefs = packages.map(() => createRef<Rect>())
  const labelRefs = packages.map(() => createRef<Txt>())

  // Build bar chart:
  const startY = -100
  packages.forEach((pkg, i) => {
    const barWidth = (pkg.downloads / maxDownloads) * 600
    const y = startY + i * 120

    view.add(
      <>
        <Txt
          ref={labelRefs[i]}
          text={pkg.name}
          x={-350}
          y={y}
          fontSize={36}
          fontWeight={600}
          fill="#ffffff"
          opacity={0}
        />
        <Rect
          ref={barRefs[i]}
          x={-50}
          y={y}
          width={0}
          height={60}
          fill={pkg.color}
          radius={8}
        />
      </>
    )
  })

  // Animate labels appearing:
  yield* sequence(
    0.15,
    ...labelRefs.map((ref) => ref().opacity(1, 0.3))
  )

  // Animate bars growing:
  yield* sequence(
    0.2,
    ...packages.map((pkg, i) => {
      const barWidth = (pkg.downloads / maxDownloads) * 600
      return barRefs[i]().width(barWidth, 1, easeOutCubic)
    })
  )

  // Add download count labels:
  for (let i = 0; i < packages.length; i++) {
    const barWidth = (packages[i].downloads / maxDownloads) * 600
    const countRef = createRef<Txt>()
    view.add(
      <Txt
        ref={countRef}
        text={`${(packages[i].downloads / 1000000).toFixed(1)}M`}
        x={-50 + barWidth + 60}
        y={startY + i * 120}
        fontSize={28}
        fill="#94a3b8"
        opacity={0}
      />
    )
    yield* countRef().opacity(1, 0.3)
  }

  yield* waitFor(2)
})

Project configuration

// vite.config.ts
import { defineConfig } from "vite"
import motionCanvas from "@motion-canvas/vite-plugin"

export default defineConfig({
  plugins: [
    motionCanvas({
      project: ["./src/project.ts"],
    }),
  ],
})

// src/project.ts
import { makeProject } from "@motion-canvas/core"
import packageStats from "./scenes/packageStats?scene"
import comparison from "./scenes/comparison?scene"

export default makeProject({
  scenes: [packageStats, comparison],
  background: "#0f172a",
})

Rendering

# Render from the editor (browser-based):
# Open http://localhost:9000 → Click "Render" → Export as MP4/image sequence

# CLI rendering:
npx motion-canvas render

# Export settings in project.ts:
export default makeProject({
  scenes: [packageStats, comparison],
  settings: {
    size: { x: 1920, y: 1080 },
    fps: 60,
    background: "#0f172a",
  },
})

Revideo

Revideo — automated video production:

Setup

npm create @revideo/app@latest my-video
cd my-video
npm start

Scene creation (Motion Canvas compatible)

// src/scenes/packageHighlight.tsx
import { makeScene2D, Txt, Rect, Img } from "@revideo/2d"
import { createRef, all, waitFor, sequence } from "@revideo/core"

export default makeScene2D(function* (view) {
  const { name, downloads, version, logoUrl } = view.variables.get("package")()

  view.fill("#0f172a")

  const title = createRef<Txt>()
  const stat = createRef<Txt>()
  const logo = createRef<Img>()

  view.add(
    <>
      {logoUrl && (
        <Img ref={logo} src={logoUrl} width={120} y={-150} opacity={0} />
      )}
      <Txt
        ref={title}
        text={name}
        fontSize={72}
        fontWeight={800}
        fill="#ffffff"
        y={-30}
        opacity={0}
      />
      <Txt
        ref={stat}
        text={`${(downloads / 1000000).toFixed(1)}M weekly downloads`}
        fontSize={36}
        fill="#94a3b8"
        y={60}
        opacity={0}
      />
    </>
  )

  // Animate:
  if (logoUrl) {
    yield* logo().opacity(1, 0.5)
  }
  yield* title().opacity(1, 0.5)
  yield* stat().opacity(1, 0.5)
  yield* waitFor(2)
})

Rendering API (server-side)

// render-server.ts — automated video rendering
import { renderVideo } from "@revideo/renderer"

// Render a single video:
const result = await renderVideo({
  projectFile: "./src/project.ts",
  settings: {
    outFile: "output/react-highlight.mp4",
    size: { x: 1920, y: 1080 },
    fps: 30,
  },
  variables: {
    package: {
      name: "React",
      downloads: 25000000,
      version: "19.0.0",
      logoUrl: "https://cdn.example.com/react.svg",
    },
  },
})

console.log(`Rendered: ${result.outputPath}`)

Template system (batch rendering)

// batch-render.ts — render videos from data
import { renderVideo } from "@revideo/renderer"

const packages = [
  { name: "React", downloads: 25000000, version: "19.0.0" },
  { name: "Vue", downloads: 4500000, version: "3.5.0" },
  { name: "Svelte", downloads: 1200000, version: "5.0.0" },
  { name: "Angular", downloads: 3200000, version: "18.0.0" },
  { name: "Next.js", downloads: 7000000, version: "15.0.0" },
]

// Render all videos in parallel:
const renders = packages.map((pkg) =>
  renderVideo({
    projectFile: "./src/project.ts",
    settings: {
      outFile: `output/${pkg.name.toLowerCase()}-highlight.mp4`,
      size: { x: 1080, y: 1920 },  // Vertical for social media
      fps: 30,
    },
    variables: { package: pkg },
  })
)

const results = await Promise.all(renders)
console.log(`Rendered ${results.length} videos`)

REST API for rendering

// api-server.ts — expose rendering as an API
import express from "express"
import { renderVideo } from "@revideo/renderer"

const app = express()
app.use(express.json())

app.post("/api/render", async (req, res) => {
  const { template, variables, format } = req.body

  try {
    const result = await renderVideo({
      projectFile: `./src/templates/${template}.ts`,
      settings: {
        outFile: `output/${Date.now()}.mp4`,
        size: format === "vertical"
          ? { x: 1080, y: 1920 }
          : { x: 1920, y: 1080 },
        fps: 30,
      },
      variables,
    })

    res.json({
      success: true,
      videoUrl: `/videos/${result.outputPath}`,
      duration: result.duration,
    })
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

// Webhook-triggered rendering:
app.post("/api/webhooks/new-package", async (req, res) => {
  const pkg = req.body

  // Auto-generate highlight video for new packages:
  const result = await renderVideo({
    projectFile: "./src/templates/package-highlight.ts",
    settings: {
      outFile: `output/highlights/${pkg.name}.mp4`,
      size: { x: 1080, y: 1920 },
      fps: 30,
    },
    variables: { package: pkg },
  })

  // Upload to CDN:
  await uploadToCDN(result.outputPath)

  res.json({ success: true })
})

app.listen(4000)

Feature Comparison

FeatureRemotionMotion CanvasRevideo
LanguageReact/TSXTypeScriptTypeScript
ParadigmReact componentsGenerator functionsGenerator functions
Editor✅ (Remotion Studio)✅ (browser-based)✅ (browser-based)
Server rendering✅ (Lambda, Node)❌ (browser only)✅ (Node.js API)
Player component✅ (@remotion/player)
Template systemVia props✅ (variables API)
Batch rendering
REST APIVia custom code✅ (built-in)
Data-driven✅ (React props)Via variables✅ (variables)
Animation controlReact + springGenerator tweensGenerator tweens
Audio support
Output formatsMP4, WebM, GIFImage sequence, MP4MP4, WebM
Cloud rendering✅ (Lambda)Self-hosted
LicenseBusiness SourceMITMIT
GitHub stars21K+16K+2K+

When to Use Each

Use Remotion if:

  • You're a React developer and want to leverage React ecosystem for video
  • Need a Player component to embed videos in web apps
  • Want cloud rendering with AWS Lambda for scale
  • Building data-driven video generation with React props

Use Motion Canvas if:

  • Want the finest control over animations with generator functions
  • Building hand-crafted explanatory videos or visualizations
  • Prefer a real-time editor for iterating on animations
  • Creating educational content or technical presentations

Use Revideo if:

  • Need server-side rendering API for automated video pipelines
  • Building a template system for batch video generation
  • Want Motion Canvas animation control with production rendering
  • Creating automated social media content or personalized videos

Methodology

Download data from npm registry (weekly average, March 2026). Feature comparison based on remotion v4.x, @motion-canvas/core v3.x, and @revideo/core v0.x.

Compare video tools and developer libraries on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.