| name | remotion-video |
| description | This skill should be used when the user asks to "make a video with Remotion", "create a programmatic/data-driven video in React", "render an MP4/GIF from code", "animate with useCurrentFrame/interpolate/spring", "sync video to audio/beats", or "render a templated video per record headlessly". Covers building React compositions and rendering them via CLI or @remotion/renderer. |
| version | 0.1.0 |
Remotion (Programmatic Video)
Build real MP4/GIF/WebM videos in React. Every frame is a pure function of useCurrentFrame(), so output is deterministic, scrubbable, diffable, and renderable in CI. The code-first alternative to After Effects for templated and data-driven motion graphics.
When to use
- Render MP4/GIF/WebM from code (social clips, title cards, explainers).
- Templated/data-driven videos: one composition, many outputs from props (per-user, per-record, per-row of a CSV/DB).
- Motion graphics that must be versioned, code-reviewed, and rendered in CI without a GUI.
- Programmatic audio sync, charts that animate from data, or embedding shaders/Three.js into video.
Core techniques
Frame-driven animation
Animation is derived from the current frame, never from setState or requestAnimationFrame. interpolate maps an input range to an output range; spring produces physically natural motion.
import {useCurrentFrame, useVideoConfig, interpolate, spring} from 'remotion';
export const Title = () => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const scale = spring({frame, fps, config: {damping: 200, mass: 1, stiffness: 100}});
return <h1 style={{opacity, transform: `scale(${scale})`}}>Hello</h1>;
};
Always clamp interpolate unless an intentional overshoot is desired — by default it extrapolates linearly past the range, which produces opacity > 1 or negative values.
Scheduling: Sequence and Series
<Sequence> shifts time: children see frame reset to 0 at the sequence's from. Use it to place clips on a timeline. <Series> lays out back-to-back segments without manual offset math.
import {Sequence, Series, AbsoluteFill} from 'remotion';
export const Timeline = () => (
<AbsoluteFill style={{backgroundColor: 'black'}}>
<Sequence from={0} durationInFrames={60}><Intro /></Sequence>
<Sequence from={60} durationInFrames={90}><Body /></Sequence>
{/* Series auto-sequences; offset overlaps the previous segment for crossfades */}
<Series>
<Series.Sequence durationInFrames={60}><ShotA /></Series.Sequence>
<Series.Sequence durationInFrames={60} offset={-15}><ShotB /></Series.Sequence>
</Series>
</AbsoluteFill>
);
Composition registration + parametric props
The <Composition> declares id, size, fps, duration, and defaultProps. A zod schema makes props type-safe and editable in the Studio sidebar.
import {Composition} from 'remotion';
import {z} from 'zod';
export const schema = z.object({
title: z.string(),
accent: z.string(),
fps: z.number().default(30),
});
export const Root = () => (
<Composition
id="Promo"
component={Promo}
durationInFrames={150}
fps={30}
width={1080}
height={1920}
schema={schema}
defaultProps={{title: 'Launch', accent: '#5b8cff', fps: 30}}
/>
);
To make duration data-dependent, use calculateMetadata on the Composition to compute durationInFrames from props (e.g. number of rows × frames per row) before render.
Audio and beat sync
import {Audio, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
const BEATS_SEC = [0.5, 1.0, 1.5, 2.0];
export const Music = ({children}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const t = frame / fps;
const onBeat = BEATS_SEC.some((b) => Math.abs(t - b) < 1 / fps);
return (
<>
<Audio src={staticFile('track.mp3')} />
<div style={{transform: `scale(${onBeat ? 1.06 : 1})`}}>{children}</div>
</>
);
};
Detect beats offline (e.g. with web-audio-beat-detector or aubio) and bake the timestamps into props — never analyze audio at render time, since headless rendering has no realtime audio clock.
Rendering
Preview in the browser-based Studio; render headlessly via CLI or the programmatic API.
npx remotion studio
npx remotion render Promo out/promo.mp4 \
--props='{"title":"Launch","accent":"#f43"}'
npx remotion render Promo out.gif --codec=gif
For batch/data-driven pipelines, render in Node with @remotion/renderer (bundle once, render many) — see the reference file.
Deliver & verify (rendered stills → MP4)
Packaged helper (scripts/): tile your stills with scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, then assert the encode with scripts/probe-mp4.sh out.mp4 [WxH] [fps]. See scripts/README.md.
Remotion is frame-deterministic by construction — every frame is a pure function of useCurrentFrame(), so you can render any exact frame headlessly with no seek harness (this is the heavy-tier counterpart to a web scene's ?t=N). Use this tier when the output must be an MP4/GIF, must carry exact numbers/text, or batches from data; for a lightweight web animation, deliver standalone HTML instead.
Output contract:
- A Remotion project with the composition registered (
<Composition> + zod schema + defaultProps), all motion frame-driven (no timers / Date.now() / Math.random()).
- Deliverable = the rendered
out/*.mp4 (plus the project, so the user can re-render with new props/data).
- Duration data-dependent? compute it in
calculateMetadata, not by hand.
Verify loop — render stills → inspect → encode. Render single frames first (cheap, no video encode), inspect them, and encode the full video only once the frames are right.
npx remotion still Promo out/f-start.png --frame=0
npx remotion still Promo out/f-mid.png --frame=75
npx remotion still Promo out/f-end.png --frame=149
npx remotion render Promo out/promo.mp4 --props='{"title":"Launch"}'
- Use
npx remotion compositions to read each composition's durationInFrames/fps and pick the end frame.
- Data-driven / batch: verify ONE representative props set via stills before batch-rendering all rows — catch a layout bug once instead of N times.
- README demo GIF for free:
npx remotion render Promo out/demo.gif --codec=gif produces the first-screen proof clip (Direction D).
Before you finish:
npx remotion still renders cleanly at frame 0, mid, and last — no errors, no missing assets/fonts.
- Numbers/text are exact and inside safe areas at every checked frame.
- Frame-driven only — no
Date.now() / Math.random() / timers (determinism holds in CI).
- Props are zod-typed; the shipped props render correctly (not just
defaultProps).
- Full MP4 encoded and plays; (optional) GIF rendered for the README.
Quick reference
| Need | Use |
|---|
| Map time → value | interpolate(frame, [a,b], [x,y], {extrapolateRight:'clamp'}) |
| Natural easing | spring({frame, fps, config}) |
| Place a clip at t | <Sequence from durationInFrames> |
| Back-to-back shots | <Series> |
| Audio | <Audio src={staticFile(...)} /> |
| Frame ↔ seconds | frame / fps |
| Loop a value | frame % period then interpolate |
| Editable props | zod schema on <Composition> |
| CI render | npx remotion render or @remotion/renderer |
Gotchas
- Never use
Math.random(), Date.now(), or animation timers — they break determinism. Use random(seed) from Remotion for stable per-frame randomness.
- Load fonts and assets via
staticFile() and wait with delayRender/continueRender, or fonts pop in mid-render.
- Default
interpolate extrapolates — clamp it.
useCurrentFrame inside a <Sequence> is local (starts at 0); use useVideoConfig().durationInFrames for absolute timing.
Reference files
references/api-and-patterns.md — interpolate options and Easing, spring config recipes, Sequence/Series scheduling, <Audio> + beat-sync, parametric defaultProps with zod + calculateMetadata, CLI flags, programmatic @remotion/renderer batch rendering, and embedding GLSL shaders / Three.js (@remotion/three).