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
The Programming Model: Declarative vs Generator-Based Animation
Remotion and Motion Canvas take fundamentally different approaches to describing animation, and the choice between them often comes down to which mental model fits the content being produced.
Remotion uses React's declarative model: you describe what the frame at any given time should look like, and the player handles the rest. If you want a title to fade in over the first 30 frames, you write interpolate(frame, [0, 30], [0, 1]) and apply the result to an opacity style prop. The entire video is essentially a React component that renders differently based on useCurrentFrame(). This model is immediately familiar to any React developer — there is no new programming concept to learn, just a new set of hooks and interpolation utilities. It's particularly well-suited to data-driven content where the same component structure renders with different values for each output.
Motion Canvas and Revideo use a generator-based imperative model: you write a generator function that yield*s animation sequences in order. yield* title().opacity(1, 0.5) means "animate the title's opacity to 1 over 0.5 seconds, then continue execution." This model maps directly to how you think about a scripted animation sequence: first this happens, then that, then wait, then the next scene. For long-form explanatory videos with dozens of sequential animation steps, the generator model produces cleaner, more readable code than equivalent Remotion code with multiple interpolate calls and frame-offset arithmetic.
Neither model is strictly superior. Remotion excels at content where data shapes the video; Motion Canvas excels at carefully choreographed sequences where animation timing is the primary concern.
Production Deployment and Rendering Infrastructure
The rendering infrastructure each framework requires shapes how it fits into production pipelines.
Remotion's server-side rendering uses a headless Chromium browser running your React code — architecturally similar to Puppeteer-based screenshot services. Each frame is rendered as a DOM/Canvas capture. The practical consequence is that Remotion can render any CSS, SVG, and web font: if it works in a browser, it works in Remotion output. The cost is that headless Chrome is memory-intensive. Rendering 150 frames at 1080p takes 8-15 seconds on a typical CI runner, and Lambda-based cloud rendering requires provisioning memory for Chrome alongside your composition code. Remotion Lambda automates this infrastructure, but each render is a real cost to plan for at scale. For teams generating thousands of videos per month, the per-render cost of Remotion Lambda is a meaningful line item.
Motion Canvas renders using a canvas-based engine, not a full DOM. The output is pixel-accurate because Motion Canvas controls every draw call directly. This makes Motion Canvas renders faster than Remotion for complex scenes that would trigger layout reflow in a full DOM, but it means Motion Canvas cannot use arbitrary HTML or CSS. Everything must be drawn using Motion Canvas's scene graph API: Txt, Rect, Circle, Img, and custom node types. The browser-based editor is Motion Canvas's primary rendering target; CLI rendering invokes the same canvas engine headlessly.
Revideo inherits Motion Canvas's canvas-based rendering and adds a Node.js API that makes server-side render calls first-class. The renderVideo() function is designed to be called from an Express server, a queue worker, or a cron job — this is Revideo's core value proposition over plain Motion Canvas. For batch video generation pipelines where videos are triggered by webhooks or database events, Revideo's architecture is the practical fit.
Licensing and Commercial Use
Licensing is a practical consideration for teams building commercial products.
Remotion uses the Business Source License (BUSL). Remotion is free for open-source projects and small companies, but commercial use at scale requires a paid company license: $0 for companies under $1M ARR, $50/month for $1M-$10M ARR, $200/month above that. This isn't unusual for developer tools with a cloud product attached, but it means Remotion has a licensing step that Motion Canvas and Revideo do not. Teams generating videos as a core product feature — not just internal tooling — should account for this in their cost model.
Motion Canvas is MIT-licensed with no commercial restrictions. You can use it in any product, for any purpose, with no fees. The trade-off is that Motion Canvas doesn't offer managed cloud rendering — you maintain your own infrastructure for server-side rendering.
Revideo is also MIT-licensed and is specifically designed for commercial automation pipelines. The Revideo team's business model is hosted cloud rendering at re.video, not license fees. Using the self-hosted Node.js rendering API is free and unrestricted, which makes Revideo the natural choice for teams that need batch video generation without a licensing overhead or the complexity of managing headless Chrome infrastructure.
Ecosystem and Tooling Integration
Remotion's React foundation gives it a ready-made ecosystem. Any React component library — Recharts for data visualization, D3 wrapped in React hooks, shadcn/ui for UI elements — can be used directly in a Remotion composition. React Query for data fetching, Zustand for state management, and React's Suspense for async data loading all work because Remotion is just React. The @remotion/google-fonts package provides first-class Google Fonts support for consistent typography across renders without manual font loading.
Motion Canvas and Revideo have smaller, framework-specific ecosystems. Their APIs are specific to the animation engine, so external React component libraries don't transfer. Custom scene elements are implemented using the Motion Canvas node API — well-designed and composable, but requiring familiarity with its specific patterns. The benefit is that Motion Canvas scenes have no external dependencies to manage; the animation code is deterministic and portable. For teams that value composability with the broader npm ecosystem, Remotion has a structural advantage that Motion Canvas cannot replicate without a fundamental architecture change.
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 →
See also: Emotion vs styled-components and AVA vs Jest, acorn vs @babel/parser vs espree.