with one click
remotion
// [User-Invoked] Use when you need create, update, or preview Remotion videos.
// [User-Invoked] Use when you need create, update, or preview Remotion videos.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | remotion |
| version | 3.1.0 |
| description | [User-Invoked] Use when you need create, update, or preview Remotion videos. |
[IMPORTANT] Use
TaskCreateto break ALL work into small tasks BEFORE starting. For simple tasks, ask user whether to skip. MUST ATTENTION wait for user approval of scene plan (Step 3.3) before writing any files — NEVER skip. MUST ATTENTION read existing project files before modifying — NEVER overwrite scenes blindly. MUST ATTENTION updatetotalChaptersin ALL existing scene files when adding/removing scenes.
Be skeptical. Apply critical thinking. Every claim needs traced proof, confidence >80% to act.
Goal: Create new Remotion video project, add/update scenes in existing one, or launch local Remotion Studio preview server.
Two Modes:
| Mode | When | Action |
|---|---|---|
| Create / Update (default) | User describes video content OR asks to update/add scenes | Scaffold new project or modify existing scenes |
| Play | User says "play", "preview", "open studio", "watch", "start server" | Find Remotion project → npm run studio |
Default project path: remotion/ (relative to workspace root). Respect any path user explicitly provides.
Key Rules:
totalChapters consistent across ALL scene filesuseCurrentFrame() — CSS transitions FORBIDDEN<Img>, <Video>, <Audio>) instead of native HTML elementsImplementing one of these? Copy from refs/ — do NOT implement from memory:
| Implementing... | Copy from |
|---|---|
src/components/Shared.tsx (new scaffold) | refs/Shared.tsx — C palette, ProgressBar, ChapterBadge, CodeBlock, Pill, AnimRow |
src/utils/animations.ts (new scaffold) | refs/animations.ts — easeOut, easeInOut, pop, staggeredEaseOut, counter |
| Typewriter or word-highlight text effect | refs/text-animations.tsx — getTypedText, Cursor, TypewriterScene, Highlight |
| TikTok-style captions with word highlight | refs/captions.tsx — CaptionedVideo, CaptionPage, delayRender, createTikTokStyleCaptions |
| Video/audio duration, dimensions, frames | refs/mediabunny-utils.ts — getVideoDuration, getAudioDuration, getVideoDimensions, canDecode, extractFrames |
| Mapbox map scene | refs/maps-mapbox.tsx — MapScene component, interactive:false, camera animation |
| ElevenLabs TTS voiceover generation | refs/generate-voiceover.ts — TTS script + calculateMetadata integration |
Classify prompt:
PLAY keywords → "play", "preview", "open studio", "start server", "watch", "launch", "dev server"
CREATE/UPDATE → everything else (default)
Ambiguous → proceed with Create/Update (safe default).
Locate Remotion project before acting:
# Check default path
ls remotion/package.json 2>/dev/null
# Check if CWD is already a Remotion project
ls package.json 2>/dev/null | xargs grep -l '"remotion"' 2>/dev/null
# Check common paths
find . -maxdepth 3 -name "package.json" -exec grep -l '"remotion"' {} \; 2>/dev/null | head -5
State after detection:
PROJECT_EXISTS = true/falsePROJECT_PATH = remotion/ (default) or detected pathHAS_STUDIO_SCRIPT = true/false (check package.json scripts.studio)When: User explicitly asks to preview / open studio / launch dev server.
If PROJECT_EXISTS = false:
"No Remotion project found. Run
/remotion <description>to create one first." Exit.
cat {PROJECT_PATH}/package.json | grep '"studio"'
# → use: npm run studio
# → fallback: npx remotion studio
cd {PROJECT_PATH} && npm run studio
Remotion Studio launches at http://localhost:3000
Server runs in background. Report URL and composition IDs visible in src/Root.tsx.
npx remotion still [composition-id] --scale=0.25 --frame=30
# At 30fps, --frame=30 = one-second mark (zero-based)
| Condition | Action |
|---|---|
PROJECT_EXISTS = false | Scaffold new project → Phase 3.2 |
PROJECT_EXISTS = true | Read existing project → Phase 3.4 |
Only when no existing Remotion project found.
# Creates {PROJECT_PATH}/ with blank template (no Tailwind)
npx create-video@latest --yes --blank --no-tailwind {PROJECT_PATH}
cd {PROJECT_PATH}
npm install @remotion/transitions # add transitions support
Replace generated src/ with project structure below (keep package.json and tsconfig.json from scaffold).
npx create-video unavailablemkdir -p {PROJECT_PATH}/src/compositions {PROJECT_PATH}/src/components {PROJECT_PATH}/src/utils
cd {PROJECT_PATH}
npm init -y
npm install remotion @remotion/cli @remotion/transitions react react-dom
npm install -D @types/react @types/react-dom typescript
{PROJECT_PATH}/
package.json ← scripts: studio, render, still
tsconfig.json
src/
index.ts ← registerRoot
Root.tsx ← register compositions
components/
Shared.tsx ← palette + reusable UI components
utils/
animations.ts ← easeOut, staggeredEaseOut, pop, counter
compositions/
{CompositionName}.tsx
package.json scripts (merge into existing after install){
"scripts": {
"studio": "remotion studio",
"render": "remotion render {CompositionId} out/video.mp4",
"still": "remotion still {CompositionId} --frame=0 out/still.png"
},
"remotion": {
"entryPoint": "src/index.ts"
}
}
tsconfig.json{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "ES2020"],
"jsx": "react-jsx",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}
src/index.tsimport { registerRoot } from 'remotion';
import { Root } from './Root';
registerRoot(Root);
src/components/Shared.tsxCopy from
refs/Shared.tsx— do NOT implement from memory. Exports:C(palette),ProgressBar,ChapterBadge,CodeBlock,Pill,AnimRow. Always create; all scenes import from here.
src/utils/animations.tsCopy from
refs/animations.ts— do NOT implement from memory. Exports:easeOut,easeInOut,pop,staggeredEaseOut,counter.
MUST ATTENTION wait for user confirmation before implementing any scenes.
Plan composition before writing files:
Scene01Intro, Scene02{Topic}, etc.Present plan before writing files:
Proposed: {N} scenes, ~{total}s total
Scene 01 ({Xs}) — {topic}: {layout description}
Scene 02 ({Xs}) — {topic}: {layout description}
…
Proceed? (or adjust)
MUST ATTENTION read existing project state before any modifications.
When PROJECT_EXISTS = true:
# Read composition registry
cat {PROJECT_PATH}/src/Root.tsx
# List scene files
ls {PROJECT_PATH}/src/compositions/ 2>/dev/null || ls {PROJECT_PATH}/src/scenes/ 2>/dev/null
# Read main composition orchestrator
cat {PROJECT_PATH}/src/ClaudeAgentExplainer.tsx 2>/dev/null # or equivalent
Identify:
totalChapters / composition durationTarget only scenes affected by user's request. Preserve all unchanged scenes.
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
import { C, ProgressBar, ChapterBadge } from '../components/Shared'; // adjust path
import { easeOut, staggeredEaseOut } from '../utils/animations'; // adjust path
// Data arrays at the top — keep out of component body
const ITEMS = [ ... ];
export const Scene{NN}{Name}: React.FC = () => {
const frame = useCurrentFrame();
return (
<AbsoluteFill style={{ background: C.bg, fontFamily: 'system-ui, -apple-system, sans-serif' }}>
<ChapterBadge index={NN} label="{Scene Name}" color={C.blue} />
<div style={{ position: 'absolute', inset: 0, display: 'flex', gap: 48, padding: '68px 72px 44px' }}>
{/* Content here */}
</div>
<ProgressBar chapterIndex={NN - 1} totalChapters={TOTAL} />
</AbsoluteFill>
);
};
| Element | Pattern |
|---|---|
| Eyebrow label | opacity: easeOut(frame, 0, 14) |
| Hero title | opacity: easeOut(frame, 8, 22) + translateY from 28→0 |
| Subtitle | opacity: easeOut(frame, 22, 18) |
| List items (stagger) | staggeredEaseOut(frame, i, startAt, 12, 16) |
| Cards (stagger up) | staggeredEaseOut + translateY(20→0) |
| Cards (stagger right) | staggeredEaseOut + translateX(28→0) |
| Late callout boxes | easeOut(frame, 90+, 18) |
Left/Right split (most common):
<div style={{ position: 'absolute', inset: 0, display: 'flex', gap: 48, padding: '68px 72px 44px' }}>
<div style={{ width: 400, flexShrink: 0, ... }}> {/* Left panel */}
<div style={{ flex: 1, ... }}> {/* Right panel */}
</div>
Full-width column:
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', padding: '68px 64px 44px', gap: 16 }}>
Centered (title/CTA scene):
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '72px 100px', textAlign: 'center' }}>
Card grid:
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12 }}>
| Element | fontSize | fontWeight | Notes |
|---|---|---|---|
| Eyebrow label | 14 | 700 | category color, letterSpacing: 3 |
| Hero title | 44–56 | 800 | C.text, lineHeight: 1.1 |
| Body text | 17–21 | 400 | C.dim, lineHeight: 1.6 |
| Card label | 15–16 | 700 | category color |
| Monospace detail | 12–14 | 400 | fontFamily: 'Courier New' |
| Card container | — | — | C.surface, borderLeft: 3px solid color, borderRadius: 10–12 |
import { TransitionSeries, linearTiming } from '@remotion/transitions';
import { fade } from '@remotion/transitions/fade';
import { Scene01Intro } from './compositions/Scene01Intro';
// ... other imports
const T = 18; // transition duration in frames
const D = {
s01: 210, // 7s
s02: 240 // 8s
// ...
};
const SCENES = Object.values(D).length;
export const TOTAL_DURATION_FRAMES = Object.values(D).reduce((a, b) => a + b, 0) - (SCENES - 1) * T;
const tr = () => <TransitionSeries.Transition presentation={fade()} timing={linearTiming({ durationInFrames: T })} />;
export const {
CompositionName
}: React.FC = () => (
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={D.s01}>
<Scene01Intro />
</TransitionSeries.Sequence>
{tr()}
<TransitionSeries.Sequence durationInFrames={D.s02}>
<Scene02Next />
</TransitionSeries.Sequence>
{/* ... */}
</TransitionSeries>
);
// Root.tsx
import { Composition } from 'remotion';
import { {CompositionName}, TOTAL_DURATION_FRAMES } from './{CompositionName}';
export const Root: React.FC = () => (
<Composition id="{CompositionId}" component={{{CompositionName}}} durationInFrames={TOTAL_DURATION_FRAMES} fps={30} width={1920} height={1080} />
);
// Root.tsx
import { Composition } from 'remotion';
import { MyScene } from './compositions/MyScene';
export const Root: React.FC = () => <Composition id="MyVideo" component={MyScene} durationInFrames={300} fps={30} width={1920} height={1080} />;
After creating/updating, report changed files and offer launch:
✅ Created {N} scene files in {PROJECT_PATH}/src/compositions/
Total duration: ~{X}s ({FRAMES} frames @ 30fps)
To preview: run `/remotion play` — starts Remotion Studio at http://localhost:3000
To render: cd {PROJECT_PATH} && npm run render
Root.tsx to find composition orchestrator and current scene counttotalChapters across existing scenesD.sNN entry<TransitionSeries.Sequence> blocktotalChapters in ALL existing scene files (+1)src/components/Shared.tsx → C objectfontFamily in AbsoluteFill style per scene (or add global in Shared)Authoritative Remotion API reference — apply when implementing beyond basic scene creation.
MUST ATTENTION ALL animations driven by useCurrentFrame(). Durations: seconds × fps.
import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion';
export const FadeIn = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: 'clamp',
extrapolateLeft: 'clamp',
easing: Easing.bezier(0.16, 1, 0.3, 1)
});
return <div style={{ opacity }}>Hello World!</div>;
};
FORBIDDEN: CSS transition-*, CSS animation-*, Tailwind transition-*/animate-* classes.
// With clamping + Bézier easing
const opacity = interpolate(frame, [0, 60], [0, 1], {
easing: Easing.bezier(0.16, 1, 0.3, 1),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
});
Copy-paste curves:
easing: Easing.bezier(0.16, 1, 0.3, 1); // crisp UI entrance (strong ease-out)
easing: Easing.bezier(0.45, 0, 0.55, 1); // editorial / slow fade
easing: Easing.bezier(0.34, 1.56, 0.64, 1); // playful overshoot
Composing multiple properties from one progress:
const slideIn = interpolate(frame, [start, start + dur], [0, 1], {
easing: Easing.bezier(0.22, 1, 0.36, 1),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
});
const overlayX = interpolate(slideIn, [0, 1], [100, 0]);
const opacity = interpolate(slideIn, [0, 1], [0, 1]);
<Sequence> delays element appearance. useCurrentFrame() inside = local frame (0-based). Always premountFor:
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>
<Series> for sequential without overlap:
<Series>
<Series.Sequence durationInFrames={45}>
<Intro />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<MainContent />
</Series.Sequence>
<Series.Sequence offset={-15} durationInFrames={60}>
<SceneB />
</Series.Sequence>{' '}
{/* overlap */}
</Series>
Trimming:
<Sequence from={-0.5 * fps}><MyAnimation /></Sequence> // skip first 15 frames
<Sequence durationInFrames={1.5 * fps}><MyAnimation /></Sequence> // trim end
<Sequence from={30}><Sequence from={-15}><MyAnimation /></Sequence></Sequence> // trim + delay
import { Composition, Folder, Still } from "remotion";
<Composition id="MyComposition" component={MyComp} durationInFrames={100} fps={30} width={1080} height={1080} />
// With default props (use `type` not `interface`)
<Composition id="MyComposition" component={MyComp} durationInFrames={100} fps={30} width={1080} height={1080}
defaultProps={{ title: "Hello World", color: "#ff0000" } satisfies MyCompositionProps}
/>
// Still (single-frame)
<Still id="Thumbnail" component={Thumbnail} width={1280} height={720} />
MUST use staticFile() for assets in public/:
import { Img, staticFile } from "remotion";
import { Video } from "@remotion/media";
import { Audio } from "@remotion/media";
<Img src={staticFile("logo.png")} />
<Video src={staticFile("clip.mp4")} />
<Audio src={staticFile("music.mp3")} />
// Remote URLs work directly — no staticFile needed
MUST use <Img> from remotion — NOT <img>, NOT Next.js <Image>, NOT CSS background-image.
import { Img, staticFile } from "remotion";
<Img src={staticFile("photo.png")} style={{ width: 500, height: 300, objectFit: "cover" }} />
<Img src={staticFile(`frames/frame${frame}.png`)} /> // dynamic paths work
Install: npx remotion add @remotion/media
import { Video } from "@remotion/media";
<Video src={staticFile("video.mp4")} />
<Video src={staticFile("video.mp4")} trimBefore={2 * fps} trimAfter={10 * fps} />
<Video src={staticFile("video.mp4")} volume={(f) => interpolate(f, [0, fps], [0, 1], { extrapolateRight: "clamp" })} />
<Video src={staticFile("video.mp4")} playbackRate={2} loop muted />
Install: npx remotion add @remotion/media
import { Audio } from "@remotion/media";
<Audio src={staticFile("audio.mp3")} />
<Audio src={staticFile("audio.mp3")} trimBefore={2 * fps} trimAfter={10 * fps} />
<Sequence from={1 * fps}><Audio src={staticFile("audio.mp3")} /></Sequence> // delay
// volume callback: `f` starts at 0 when audio begins, not composition frame
<Audio src={staticFile("audio.mp3")} volume={(f) => interpolate(f, [0, fps], [0, 1], { extrapolateRight: "clamp" })} />
import { AnimatedImage, staticFile } from "remotion";
// Preferred: supports GIF, APNG, AVIF, WebP
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} />
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" playbackRate={2} />
// Fallback: GIF only — npx remotion add @remotion/gif
import { Gif } from "@remotion/gif";
<Gif src={staticFile("animation.gif")} width={500} height={500} />
Google Fonts (recommended): npx remotion add @remotion/google-fonts
import { loadFont } from '@remotion/google-fonts/Roboto';
const { fontFamily } = loadFont('normal', { weights: ['400', '700'], subsets: ['latin'] });
<h1 style={{ fontFamily, fontSize: 80, fontWeight: 'bold' }}>{text}</h1>;
Local fonts: npx remotion add @remotion/fonts
import { loadFont } from '@remotion/fonts';
import { staticFile } from 'remotion';
await Promise.all([
loadFont({ family: 'Inter', url: staticFile('Inter-Regular.woff2'), weight: '400' }),
loadFont({ family: 'Inter', url: staticFile('Inter-Bold.woff2'), weight: '700' })
]);
npx remotion add @remotion/transitions
import { TransitionSeries, linearTiming, springTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
import { flip } from "@remotion/transitions/flip";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}><SceneA /></TransitionSeries.Sequence>
<TransitionSeries.Transition presentation={fade()} timing={linearTiming({ durationInFrames: 15 })} />
<TransitionSeries.Sequence durationInFrames={60}><SceneB /></TransitionSeries.Sequence>
</TransitionSeries>
// Slide directions: "from-left" | "from-right" | "from-top" | "from-bottom"
<TransitionSeries.Transition presentation={slide({ direction: "from-left" })} timing={linearTiming({ durationInFrames: 20 })} />
// Spring timing
springTiming({ config: { damping: 200 }, durationInFrames: 25 })
// Overlay (no duration shortening — note: cannot be adjacent to transition or another overlay)
<TransitionSeries.Overlay durationInFrames={20} offset={0}><MyEffect /></TransitionSeries.Overlay>
Duration: two 60-frame scenes + 15-frame transition = 60 + 60 - 15 = 105 frames total.
Requires Remotion ≥ 4.0.415. Check: npx remotion versions. Upgrade: npx remotion upgrade.
npx remotion add @remotion/light-leaks
import { LightLeak } from "@remotion/light-leaks";
// As TransitionSeries overlay
<TransitionSeries.Overlay durationInFrames={30}><LightLeak /></TransitionSeries.Overlay>
// Standalone decorative overlay
<AbsoluteFill>
<MyContent />
<LightLeak durationInFrames={60} seed={3} />
</AbsoluteFill>
// seed: different patterns; hueShift: 0=yellow-orange, 120=green, 240=blue
<LightLeak seed={5} hueShift={240} />
npx remotion add @remotion/three
MUST wrap in <ThreeCanvas> with width and height props.
FORBIDDEN: useFrame() from @react-three/fiber — causes flickering. Use useCurrentFrame().
import { ThreeCanvas } from '@remotion/three';
import { useVideoConfig, useCurrentFrame } from 'remotion';
const { width, height } = useVideoConfig();
const frame = useCurrentFrame();
<ThreeCanvas width={width} height={height}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={0.8} />
<mesh rotation={[0, frame * 0.02, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
</ThreeCanvas>;
// <Sequence> inside ThreeCanvas must use layout="none"
All text animations driven by useCurrentFrame(). CSS transitions FORBIDDEN.
Typewriter — use string slicing, NEVER per-character opacity. Spring word highlight — scaleX wipe from left.
Implementing typewriter or word-highlight? Copy from
refs/text-animations.tsx— do NOT implement from memory. Contains:getTypedText,Cursor,TypewriterScene,Highlight.
npx remotion add @remotion/layout-utils
import { measureText, fitText, fillTextBox } from '@remotion/layout-utils';
const { width, height } = measureText({ text: 'Hello World', fontFamily: 'Arial', fontSize: 32, fontWeight: 'bold' });
const { fontSize } = fitText({ text: 'Hello World', withinWidth: 600, fontFamily: 'Inter', fontWeight: 'bold' });
MUST ATTENTION call only after fonts loaded. Use validateFontIsLoaded: true. Match font properties exactly between measurement and rendering.
Remotion applies scale() transform — use useCurrentScale() to get true dimensions:
import { useCurrentScale } from 'remotion';
const scale = useCurrentScale();
const rect = ref.current.getBoundingClientRect();
const trueWidth = rect.width / scale;
npx remotion add @remotion/captions
Caption type: { text, startMs, endMs, timestampMs, confidence }
Transcribe with Whisper.cpp: npx remotion add @remotion/install-whisper-cpp
import { installWhisperCpp, downloadWhisperModel, transcribe, toCaptions } from '@remotion/install-whisper-cpp';
await installWhisperCpp({ to: './whisper.cpp', version: '1.5.5' });
await downloadWhisperModel({ model: 'medium.en', folder: './whisper.cpp' });
// Convert to 16KHz wav first: npx remotion ffmpeg -i input.mp4 -ar 16000 output.wav
const whisperOutput = await transcribe({
model: 'medium.en',
whisperPath: './whisper.cpp',
whisperCppVersion: '1.5.5',
inputPath: '/path/to/audio.wav',
tokenLevelTimestamps: true
});
const { captions } = toCaptions({ whisperCppOutput: whisperOutput });
Import from.srt:
import { parseSrt } from '@remotion/captions';
const { captions } = parseSrt({ input: await fetch(staticFile('subtitles.srt')).then(r => r.text()) });
TikTok-style word highlight display:
Implementing TikTok-style captions? Copy from
refs/captions.tsx— do NOT implement from memory. Contains:CaptionedVideo,CaptionPage,delayRender,createTikTokStyleCaptions, per-token active highlighting.
Note: use whiteSpace: "pre" — captions are whitespace-sensitive.
import { CalculateMetadataFunction } from 'remotion';
const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props, abortSignal }) => {
const durationInSeconds = await getVideoDuration(props.videoSrc);
return { durationInFrames: Math.ceil(durationInSeconds * 30) };
};
// Transform props (fetch data before render)
const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props, abortSignal }) => {
const data = await fetch(props.dataUrl, { signal: abortSignal }).then(r => r.json());
return { props: { ...props, fetchedData: data } };
};
// Return values (all optional): durationInFrames, width, height, fps, props, defaultOutName, defaultCodec
import { z } from 'zod';
import { zColor } from '@remotion/zod-types';
export const MyCompositionSchema = z.object({
title: z.string(),
color: zColor() // renders color picker in Studio
});
// Root.tsx
<Composition
id="MyComposition"
component={MyComponent}
schema={MyCompositionSchema}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
defaultProps={{ title: 'Hello World', color: '#ff0000' }}
/>;
Top-level type MUST be z.object().
npx remotion add mediabunny
Need video/audio duration, dimensions, or frame extraction? Copy from
refs/mediabunny-utils.ts— do NOT implement from memory. Contains:getVideoDuration,getAudioDuration,getVideoDimensions,canDecode,extractFrames.
Use staticFile() for local files. Use FileSource instead of UrlSource in Node.js/Bun.
npx remotion ffmpeg -i input.mp4 output.mp3
npx remotion ffprobe input.mp4
Trimming — prefer <Video> props (non-destructive, no re-encoding):
<Video src={staticFile('video.mp4')} trimBefore={5 * fps} trimAfter={10 * fps} />
// FFmpeg fallback (MUST re-encode to avoid frozen frames)
// npx remotion ffmpeg -ss 00:00:05 -i public/input.mp4 -to 00:00:10 -c:v libx264 -c:a aac public/output.mp4
# Step 1: measure loudness
npx remotion ffmpeg -i public/video.mov -map 0:a -af loudnorm=print_format=json -f null /dev/null
# → read input_i (integrated loudness dB) and input_thresh
# Step 2: detect silences (d=0.5 = min silence duration in seconds)
npx remotion ffmpeg -i public/video.mov -map 0:a -af "silencedetect=noise=${THRESH}dB:d=0.5" -f null /dev/null
<Video src={staticFile('video.mov')} trimBefore={Math.floor(leadingEnd * fps)} trimAfter={Math.ceil(trailingStart * fps)} />
npx remotion add @remotion/media-utils
import { useWindowedAudioData, visualizeAudio, visualizeAudioWaveform, createSmoothSvgPath } from '@remotion/media-utils';
const { audioData, dataOffsetInSeconds } = useWindowedAudioData({ src: staticFile('music.mp3'), frame, fps, windowInSeconds: 30 });
if (!audioData) return null;
// Spectrum bars (numberOfSamples must be power of 2)
const frequencies = visualizeAudio({ fps, frame, audioData, numberOfSamples: 256, optimizeFor: 'speed', dataOffsetInSeconds });
// Values 0-1; left=bass, right=highs
// Waveform SVG
const waveform = visualizeAudioWaveform({ fps, frame, audioData, numberOfSamples: 256, windowInSeconds: 0.5, dataOffsetInSeconds });
const path = createSmoothSvgPath({ points: waveform.map((y, i) => ({ x: (i / (waveform.length - 1)) * width, y: 100 + y * 100 })) });
// Bass-reactive scale
const bassIntensity = frequencies.slice(0, 32).reduce((sum, v) => sum + v, 0) / 32;
const scale = 1 + bassIntensity * 0.5;
MUST ATTENTION pass frame from parent to child visualization — NEVER call useCurrentFrame() in each child inside <Sequence>.
Logarithmic scaling: const db = 20 * Math.log10(value); const scaled = (db - (-100)) / ((-30) - (-100));
npx remotion add @remotion/lottie
import { Lottie, LottieAnimationData } from '@remotion/lottie';
import { cancelRender, continueRender, delayRender } from 'remotion';
export const MyAnimation = () => {
const [handle] = useState(() => delayRender('Loading Lottie animation'));
const [animationData, setAnimationData] = useState<LottieAnimationData | null>(null);
useEffect(() => {
fetch('https://assets4.lottiefiles.com/packages/lf20_zyquagfl.json')
.then(r => r.json())
.then(json => {
setAnimationData(json);
continueRender(handle);
})
.catch(err => {
cancelRender(err);
});
}, [handle]);
if (!animationData) return null;
return <Lottie animationData={animationData} style={{ width: 400, height: 400 }} />;
};
MUST ATTENTION drive all animations from useCurrentFrame(). Disable all third-party chart library animations.
import { spring, evolvePath, getLength, getPointAtLength, getTangentAtLength } from '@remotion/paths';
// Bar chart — spring stagger
const bars = data.map((item, i) => {
const height = spring({ frame, fps, delay: i * 5, config: { damping: 200 } });
return <div style={{ height: height * item.value }} />;
});
// Line chart path animation (npx remotion add @remotion/paths)
const pathProgress = interpolate(frame, [0, 2 * fps], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });
const { strokeDasharray, strokeDashoffset } = evolvePath(pathProgress, svgPath);
<path d={svgPath} fill="none" stroke="#FF3232" strokeWidth={4} strokeDasharray={strokeDasharray} strokeDashoffset={strokeDashoffset} />;
npm i mapbox-gl @turf/turf @types/mapbox-gl — set REMOTION_MAPBOX_TOKEN in .env.
Implementing Mapbox map scene? Copy from
refs/maps-mapbox.tsx— do NOT implement from memory. Key rules:interactive: false,fadeDuration: 0, explicitwidth/height/position: "absolute"on container, animate camera viauseCurrentFrame(), NO cleanup (_map.remove()). Render:npx remotion render --gl=angle --concurrency=1.
# ProRes (for video editors)
npx remotion render --image-format=png --pixel-format=yuva444p10le --codec=prores --prores-profile=4444 MyComp out.mov
# WebM VP9 (for browsers)
npx remotion render --image-format=png --pixel-format=yuva420p --codec=vp9 MyComp out.webm
Implementing ElevenLabs TTS voiceover? Copy from
refs/generate-voiceover.ts— do NOT implement from memory. IncludescalculateMetadataintegration to size composition to audio duration.
import { Audio } from '@remotion/sfx';
<Audio src={'https://remotion.media/whoosh.wav'} />;
Available: whoosh, whip, page-turn, switch, mouse-click, shutter-modern, shutter-old, ding, bruh, vine-boom, windows-xp-error — all at https://remotion.media/{name}.wav
Use if installed. NEVER use transition-* or animate-* Tailwind classes — animate via useCurrentFrame().
totalChapters in ALL scene files when adding/removing scenes — one missed file causes visual regressionconst ITEMS inside component functionnpx create-video@latest --yes --blank --no-tailwind for scaffold — NEVER npm init unless fallback neededstaggeredEaseOut for list/card reveals — NEVER all-at-once opacityPROJECT_EXISTS before Play mode — report missing project and exituseCurrentFrame() — CSS transitions, CSS animations, Tailwind animate/transition classes FORBIDDEN<Img>, <Video>, <Audio> — NEVER native HTML elementsstaticFile() for all public/ folder assets — NEVER raw relative pathsrefs/ file — NEVER implement text animations, captions, mediabunny, maps, or voiceover from memory