Remotion vs Motion Canvas vs Revideo: Programmatic Video (2026)
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
| Feature | Remotion | Motion Canvas | Revideo |
|---|---|---|---|
| Language | React/TSX | TypeScript | TypeScript |
| Paradigm | React components | Generator functions | Generator functions |
| Editor | ✅ (Remotion Studio) | ✅ (browser-based) | ✅ (browser-based) |
| Server rendering | ✅ (Lambda, Node) | ❌ (browser only) | ✅ (Node.js API) |
| Player component | ✅ (@remotion/player) | ❌ | ❌ |
| Template system | Via props | ❌ | ✅ (variables API) |
| Batch rendering | ✅ | ❌ | ✅ |
| REST API | Via custom code | ❌ | ✅ (built-in) |
| Data-driven | ✅ (React props) | Via variables | ✅ (variables) |
| Animation control | React + spring | Generator tweens | Generator tweens |
| Audio support | ✅ | ✅ | ✅ |
| Output formats | MP4, WebM, GIF | Image sequence, MP4 | MP4, WebM |
| Cloud rendering | ✅ (Lambda) | ❌ | Self-hosted |
| License | Business Source | MIT | MIT |
| GitHub stars | 21K+ | 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.