Build professional tutorial videos programmatically with React, AI voiceover, and zero video editing software. The complete 2026 guide.
By the end of this guide, you'll have a fully code-driven video production pipeline:
Make sure you have the following ready:
node --version.
Remotion is a React framework that turns components into video frames. Every frame of your video is a React render — which means you can use all of React's power (props, state, animations) to build videos.
Scaffold a new project:
npx create-video@latest
When prompted, select these options:
Then install dependencies:
cd my-video
npm install
We need the ElevenLabs SDK for text-to-speech:
npm install elevenlabs
And make sure ffmpeg is available:
# Ubuntu/Debian
sudo apt install ffmpeg -y
# macOS
brew install ffmpeg
# Verify
ffmpeg -version
src/ for React components, public/ for audio files, and scripts/ for the voiceover generation script.Remotion videos are built from three core concepts:
useCurrentFrame() — Returns the current frame number (0, 1, 2, ...)useVideoConfig() — Returns fps, width, height, and total durationinterpolate() — Smoothly animate values between keyframesspring() — Physics-based animations with natural easingmy-video/
├── src/
│ ├── Root.tsx # Register compositions here
│ ├── components/
│ │ └── TutorialSlide.tsx # Reusable slide component
│ ├── compositions/
│ │ └── MyGuide.tsx # Video composition
│ └── index.ts
├── public/
│ └── audio/ # Generated voiceover MP3s
├── scripts/
│ └── generate-audio.mjs # ElevenLabs TTS script
└── package.json
Create a reusable slide component that supports animated titles, staggered bullet points, code blocks, and smooth transitions. This is the core building block of your video.
// src/components/TutorialSlide.tsx
import React from 'react';
import {
AbsoluteFill, interpolate,
useCurrentFrame, useVideoConfig, spring,
} from 'remotion';
interface TutorialSlideProps {
title: string;
subtitle?: string;
bulletPoints?: string[];
codeBlock?: string;
stepNumber?: number;
accentColor?: string;
}
export const TutorialSlide: React.FC<TutorialSlideProps> = ({
title, subtitle, bulletPoints = [],
codeBlock, stepNumber, accentColor = '#10b981',
}) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Fade in title
const titleOpacity = interpolate(
frame, [0, 15], [0, 1],
{ extrapolateRight: 'clamp' }
);
// Spring animation for title position
const titleY = spring({
frame, fps, from: -30, to: 0, durationInFrames: 20,
});
// Fade out at end
const exitOpacity = interpolate(
frame,
[durationInFrames - 15, durationInFrames],
[1, 0],
{ extrapolateLeft: 'clamp' }
);
return (
<AbsoluteFill style={{
background: 'linear-gradient(135deg, #0f172a, #1e293b)',
fontFamily: 'Inter, sans-serif',
padding: 60,
opacity: exitOpacity,
}}>
<h1 style={{
fontSize: 44, fontWeight: 800, color: 'white',
opacity: titleOpacity,
transform: \`translateY(\${titleY}px)\`,
}}>{title}</h1>
{/* Staggered bullet points */}
{bulletPoints.map((point, i) => {
const delay = 15 + i * 8;
const opacity = interpolate(
frame, [delay, delay + 10], [0, 1],
{ extrapolateRight: 'clamp' }
);
return (
<div key={i} style={{
opacity, fontSize: 24, color: '#e2e8f0',
marginTop: 12,
}}>• {point}</div>
);
})}
{/* Animated code block */}
{codeBlock && (
<pre style={{
background: '#0d1117', borderRadius: 12,
padding: 24, marginTop: 24,
fontSize: 20, color: '#e6edf3',
opacity: interpolate(
frame, [20, 35], [0, 1],
{ extrapolateRight: 'clamp' }
),
}}>{codeBlock}</pre>
)}
</AbsoluteFill>
);
};
interpolate(frame, [start, end], [from, to]) maps the current frame to a value. Use it for opacity, position, scale — anything you want to animate.Before generating audio, write the voiceover text for each slide. Keep it:
Structure your narration as a JSON array:
const slides = [
{
id: 'intro',
text: "Welcome to Vibe Coding Academy. Today you'll learn
how to create videos with code using Remotion."
},
{
id: 'step1',
text: "First, create a new Remotion project by running
npx create-video at latest in your terminal."
},
// ... one entry per slide
];
Create a script that calls the ElevenLabs API to generate MP3 files for each slide:
// scripts/generate-audio.mjs
import fs from 'fs';
const VOICE_ID = 'your-voice-id';
const API_KEY = process.env.ELEVENLABS_API_KEY;
async function generateAudio(text, outputPath) {
const res = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`,
{
method: 'POST',
headers: {
'xi-api-key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text,
model_id: 'eleven_multilingual_v2',
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
},
}),
}
);
const buffer = Buffer.from(await res.arrayBuffer());
fs.writeFileSync(outputPath, buffer);
console.log(`✅ ${outputPath}`);
}
// Generate for each slide
for (const slide of slides) {
await generateAudio(
slide.text,
`public/audio/${slide.id}.mp3`
);
// Rate limit delay
await new Promise(r => setTimeout(r, 500));
}
Run it:
ELEVENLABS_API_KEY=your_key node scripts/generate-audio.mjs
ElevenLabs offers dozens of pre-built voices, plus you can clone your own. List available voices:
curl -s "https://api.elevenlabs.io/v1/voices" \
-H "xi-api-key: YOUR_KEY" | python3 -c "
import sys, json
for v in json.load(sys.stdin)['voices']:
print(f\"{v['voice_id']} {v['name']}\")"
Good options for tutorials:
This is the step most tutorials skip — and it's the most important one. If you hardcode slide durations, your audio will be out of sync with your visuals.
Use ffprobe to measure the exact duration of each audio file:
# Get audio duration in seconds
ffprobe -v error -show_entries format=duration \
-of csv=p=0 public/audio/intro.mp3
# Output: 14.811429
Convert to frames:
# Formula: frames = ceil(duration × fps) + buffer
# At 30fps: ceil(14.81 × 30) + 15 = 460 frames
# Batch measure all audio files:
for f in public/audio/*.mp3; do
dur=$(ffprobe -v error -show_entries format=duration \
-of csv=p=0 "$f")
frames=$(python3 -c \
"import math; print(math.ceil(${dur} * 30) + 15)")
echo "$(basename $f .mp3): ${dur}s → ${frames} frames"
done
Wire up your slides with audio using Remotion's Sequence and Audio components:
// src/compositions/MyGuide.tsx
import React from 'react';
import { Audio, Sequence, staticFile } from 'remotion';
import { TutorialSlide } from '../components/TutorialSlide';
// Durations from ffprobe (Step 6)
const slides = [
{
id: 'intro',
durationInFrames: 460, // Measured!
slideProps: {
title: 'My Tutorial',
subtitle: 'Learn something awesome',
},
},
{
id: 'step1',
durationInFrames: 486, // Measured!
slideProps: {
title: 'Step 1: Install',
codeBlock: 'npm install my-package',
stepNumber: 1,
},
},
// ... more slides with measured durations
];
export const MyGuide: React.FC = () => {
let frameOffset = 0;
return (
<>
{slides.map((slide) => {
const start = frameOffset;
frameOffset += slide.durationInFrames;
return (
<React.Fragment key={slide.id}>
<Sequence from={start}
durationInFrames={slide.durationInFrames}>
<TutorialSlide {...slide.slideProps} />
</Sequence>
<Sequence from={start}
durationInFrames={slide.durationInFrames}>
<Audio src={staticFile(
`audio/${slide.id}.mp3`
)} />
</Sequence>
</React.Fragment>
);
})}
</>
);
};
export const TOTAL_DURATION = slides.reduce(
(sum, s) => sum + s.durationInFrames, 0
);
// src/Root.tsx
import { Composition } from 'remotion';
import { MyGuide, TOTAL_DURATION }
from './compositions/MyGuide';
export const RemotionRoot: React.FC = () => (
<Composition
id="MyGuide"
component={MyGuide}
durationInFrames={TOTAL_DURATION}
fps={30}
width={1920}
height={1080}
/>
);
Remotion includes a studio UI where you can scrub through your video, preview animations, and fine-tune timing:
npm run dev
This opens Remotion Studio in your browser. You can play, pause, and scrub frame-by-frame.
When you're happy with the preview, render the final video:
npx remotion render MyGuide out/my-guide.mp4
Useful render options:
--concurrency=2 — Use more CPU cores (increase on powerful machines)--codec=h264 — Default, widely compatible--quality=80 — Adjust quality (0-100)--concurrency=2 on servers with limited RAM to avoid out-of-memory kills.You can also render single frames as images using Remotion's Still component — perfect for YouTube thumbnails:
# Register a Still in Root.tsx, then:
npx remotion still MyThumbnail out/thumbnail.png
With the Remotion Agent Skills installed (from Step 1), Claude Code understands Remotion's API and can help you build videos faster.
Things Claude Code can do for you:
interpolate() and spring()Create a new Remotion composition that shows a code
editor animation. The code should appear character by
character, like someone is typing it. Use a dark theme
with green text. Add a blinking cursor at the end.
Claude Code will write the full React component using Remotion's API, following the best practices from the installed skills.
claude. The Agent Skills will automatically load.Here's the complete workflow from start to finish:
node scripts/generate-audio.mjs to create MP3 files via ElevenLabs.
ffprobe to get exact frame counts for each slide.
npm run dev to scrub through in Remotion Studio.
npx remotion render to produce the final MP4.
accentColor in your slide component to match your brand<Audio> component with a lower volume trackinterpolate() with different easing for slide transitions<Img> component positioned in the corner of every slideJoin the Vibe Coding Academy community to learn how to build AI-powered apps, automate your workflow, and level up your skills.
Join the Academy