| name | remotion |
| description | Build programmatic videos with Remotion (React). Covers compositions, animations, sequencing, transitions, audio/video embedding, spring physics, text animations, voiceover generation with ElevenLabs TTS, FFmpeg integration, captions/subtitles, 3D with Three.js, charts, and rendering. Based on official Remotion best practices. |
| version | 1.0.0 |
| category | build |
| platforms | ["CLAUDE_CODE","CURSOR"] |
| permissions | ["filesystem","shell"] |
Remotion -- Video Creation in React
Use this skill when working with Remotion code. Remotion lets you create videos programmatically using React components.
Project Setup
npx create-video@latest my-video
cd my-video
npm start
npm run render
Compositions
A <Composition> defines a renderable video with dimensions, fps, and duration.
import { Composition } from "remotion";
import { MyVideo } from "./MyVideo";
export const RemotionRoot = () => (
<Composition
id="MyVideo"
component={MyVideo}
durationInFrames={300}
fps={30}
width={1920}
height={1080}
defaultProps={{ title: "Hello World" } satisfies MyVideoProps}
/>
);
Use type declarations for props (not interface) for defaultProps type safety.
Use <Folder> to organize compositions in the sidebar. Use <Still> for single-frame outputs (thumbnails, OG images).
Animations
All animations MUST use useCurrentFrame(). CSS transitions and Tailwind animation classes are FORBIDDEN -- they will not render correctly.
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
export const FadeIn = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: "clamp",
});
return <div style={{ opacity }}>Hello World!</div>;
};
Spring Animations
Springs provide natural motion. They animate from 0 to 1.
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps });
const smooth = spring({ frame, fps, config: { damping: 200 } });
const snappy = spring({ frame, fps, config: { damping: 20, stiffness: 200 } });
const bouncy = spring({ frame, fps, config: { damping: 8 } });
const heavy = spring({ frame, fps, config: { damping: 15, stiffness: 80, mass: 2 } });
Use delay parameter to delay spring start:
const entrance = spring({ frame, fps, delay: 20 });
Interpolation
const opacity = interpolate(frame, [0, 100], [0, 1], {
extrapolateRight: "clamp",
extrapolateLeft: "clamp",
});
const y = interpolate(frame, [0, 30, 60, 90], [100, 0, 0, -100]);
Sequencing
Sequence
Use <Sequence> to delay when elements appear. Always use premountFor to preload components.
import { Sequence } from "remotion";
const { fps } = useVideoConfig();
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Subtitle />
</Sequence>
Use layout="none" to prevent absolute positioning wrapper.
Series
Use <Series> for sequential playback without overlap:
import { Series } from "remotion";
<Series>
<Series.Sequence durationInFrames={45}><Intro /></Series.Sequence>
<Series.Sequence durationInFrames={60}><MainContent /></Series.Sequence>
<Series.Sequence durationInFrames={30}><Outro /></Series.Sequence>
</Series>
Negative offset creates overlapping transitions:
<Series.Sequence offset={-15} durationInFrames={60}>
<SceneB />
</Series.Sequence>
Transitions
npx remotion add @remotion/transitions
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>
Light Leak Overlays
import { LightLeak } from "@remotion/light-leaks";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Overlay durationInFrames={20}>
<LightLeak />
</TransitionSeries.Overlay>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>
An overlay cannot be adjacent to a transition or another overlay.
Audio
npx remotion add @remotion/media
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
<Audio src={staticFile("audio.mp3")} />
<Audio src={staticFile("audio.mp3")} trimBefore={2 * fps} trimAfter={10 * fps} />
<Audio src={staticFile("audio.mp3")} volume={0.5} />
<Audio src={staticFile("audio.mp3")} volume={(f) =>
interpolate(f, [0, 30], [0, 1], { extrapolateRight: "clamp" })
} />
<Audio src={staticFile("audio.mp3")} playbackRate={1.5} />
<Sequence from= * }>
Multiple <Audio> components layer automatically.
Video Embedding
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")} playbackRate={2} volume={0.5} loop />
import { OffthreadVideo } from "remotion";
<OffthreadVideo src={staticFile("video.mp4")} />
ALWAYS use <OffthreadVideo> for better rendering performance. Use <Video> only during preview.
Voiceover with ElevenLabs TTS
Generate speech per scene, then use calculateMetadata to size the composition dynamically.
Generate Audio
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: "POST",
headers: {
"xi-api-key": process.env.ELEVENLABS_API_KEY!,
"Content-Type": "application/json",
Accept: "audio/mpeg",
},
body: JSON.stringify({
text: "Welcome to the show.",
model_id: "eleven_multilingual_v2",
voice_settings: { stability: 0.5, similarity_boost: 0.75, style: 0.3 },
}),
},
);
const audioBuffer = Buffer.from(await response.arrayBuffer());
writeFileSync(`public/voiceover/scene-01.mp3`, audioBuffer);
Dynamic Duration
import { CalculateMetadataFunction, staticFile } from "remotion";
const FPS = 30;
const SCENES = ["voiceover/scene-01.mp3", "voiceover/scene-02.mp3"];
export const calculateMetadata: CalculateMetadataFunction<Props> = async () => {
const durations = await Promise.all(
SCENES.map((file) => getAudioDuration(staticFile(file)))
);
const totalFrames = durations.reduce((sum, d) => sum + Math.ceil(d * FPS), 0);
return { durationInFrames: totalFrames };
};
Assets
Place files in public/ and reference with staticFile():
import { staticFile, Img } from "remotion";
<Img src={staticFile("logo.png")} />
NEVER use require() or import for media files. Always use staticFile().
Fonts
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont();
const { fontFamily } = loadFont({
url: staticFile("fonts/MyFont.woff2"),
display: "swap",
});
FFmpeg in Remotion
FFmpeg is bundled -- no install needed:
bunx remotion ffmpeg -i input.mp4 output.mp3
bunx remotion ffprobe input.mp4
For trimming, prefer the trimBefore/trimAfter props over FFmpeg. Use FFmpeg for format conversion or operations Remotion cannot do natively.
When using FFmpeg to trim, MUST re-encode to avoid frozen frames:
bunx remotion ffmpeg -ss 00:00:05 -i public/input.mp4 -to 00:00:10 -c:v libx264 -c:a aac public/output.mp4
Rendering
npx remotion render MyVideo output.mp4
npx remotion still MyVideo --frame=100 thumbnail.png
npx remotion render MyVideo output.mp4 --props='{"title": "Custom"}'
npx remotion render MyVideo output.webm --codec=vp8
Common Patterns
Scene-Based Video
const SCENES = [
{ component: TitleScene, duration: 90 },
{ component: ProblemScene, duration: 150 },
{ component: SolutionScene, duration: 120 },
{ component: CTAScene, duration: 90 },
];
export const MyVideo = () => {
let offset = 0;
return (
<>
{SCENES.map(({ component: Component, duration }, i) => {
const from = offset;
offset += duration;
return (
<Sequence key={i} from={from} durationInFrames={duration}>
<Component />
</Sequence>
);
})}
</>
);
};
Background Music + Voiceover
<>
{}
<Audio src={staticFile("music/bg.mp3")} volume={0.12} loop />
{}
<Sequence from={0} durationInFrames={scene1Frames}>
<Audio src={staticFile("voiceover/scene-01.mp3")} volume={1} />
</Sequence>
<Sequence from={scene1Frames} durationInFrames={scene2Frames}>
<Audio src={staticFile("voiceover/scene-02.mp3")} volume={1} />
</Sequence>
</>
Key Rules
- No CSS animations -- all motion via
useCurrentFrame() + interpolate()/spring()
- No Tailwind animation classes -- they don't render correctly
- Always
staticFile() for assets, never require()
- Always premount Sequences with
premountFor
- Use
<OffthreadVideo> not <video> for rendering
- Write durations in seconds and multiply by
fps
- Use
satisfies keyword for defaultProps type safety
Source: remotion-dev/skills, remotion.dev