
Remotion Best Practices
- 12 installs
- 194 repo stars
- Updated May 3, 2026
- remotion-dev/html-in-canvas
This is a copy of remotion-best-practices by remotion-dev - installs and ranking accrue to the original listing.
Embed Three.js / React Three Fiber 3D scenes in Remotion compositions without flicker during video render.
About
Remotion 3D best practices is an agent skill for solo builders shipping programmatic video who want real Three.js scenes inside Remotion. It tells your coding agent how to wire `@remotion/three`, size a `ThreeCanvas` to the composition, light meshes correctly, and keep every motion tied to `useCurrentFrame()` so server-side renders match the preview. Without those rules, 3D layers often flicker or drift between frames when Remotion encodes. The skill is narrow but critical: it does not teach general 3D art direction—it encodes the failure modes Remotion introduces when React Three Fiber meets frame-based rendering. Use it whenever you add spheres, models, or custom shaders to a Remotion timeline and want copy-paste-correct TSX patterns your agent can apply in one pass.
- Install @remotion/three via `npx remotion add` (npm/bun/yarn/pnpm variants)
- Wrap all 3D content in `<ThreeCanvas>` with explicit `width` and `height` from `useVideoConfig()`
- Require ambient + directional lighting inside ThreeCanvas
- Ban self-animating shaders/models—only `useCurrentFrame()`-driven motion (not raw `useFrame()` for render sync)
- Follow standard R3F/Three.js patterns with Remotion-specific exceptions documented in SKILL.md
Remotion Best Practices by the numbers
- 12 all-time installs (skills.sh)
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/remotion-dev/html-in-canvas --skill remotion-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 194 |
| Security audit | 2 / 3 scanners passed |
| Last updated | May 3, 2026 |
| Repository | remotion-dev/html-in-canvas ↗ |
What it does
Embed Three.js / React Three Fiber 3D scenes in Remotion compositions without flicker during video render.
Files
When to use
Use this skills whenever you are dealing with Remotion code to obtain the domain-specific knowledge.
New project setup
When in an empty folder or workspace with no existing Remotion project, scaffold one using:
npx create-video@latest --yes --blank --no-tailwind my-videoReplace my-video with a suitable project name.
Designing a video
Animate properties using useCurrentFrame() and interpolate(). Use Easing to customize the timing of the animation.
import { useCurrentFrame, 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>;
};CSS transitions or animations are FORBIDDEN - they will not render correctly. Tailwind animation class names are FORBIDDEN - they will not render correctly.
Place assets in the public/ folder at your project root.
Use staticFile() to reference files from the public/ folder.
Add images using the <Img> component:
import { Img, staticFile } from "remotion";
export const MyComposition = () => {
return <Img src={staticFile("logo.png")} style={{ width: 100, height: 100 }} />;
};Add videos using the <Video> component from @remotion/media:
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Video src={staticFile("video.mp4")} style={{ opacity: 0.5 }} />;
};Add audio using the <Audio> component from @remotion/media:
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("audio.mp3")} />;
};Assets can be also referenced as remote URLs:
import { Video } from "@remotion/media";
export const MyComposition = () => {
return <Video src="https://remotion.media/video.mp4" />
};To delay content wrap it in <Sequence> and use from. To limit the duration of an element, use durationInFrames of <Sequence>. <Sequence> by default is an absolute fill. For inline content, use layout="none".
import { Sequence } from "remotion";
export const Title = () => {
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 }}>Title</div>;
};
export const Subtitle = () => {
return <div>Subtitle</div>;
};
const Main = () => {
const {fps} = useVideoConfig();
return (
<AbsoluteFill>
<Sequence>
<Background />
</Sequence>
<Sequence from={1 * fps} durationInFrames={2 * fps} layout="none">
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} layout="none">
<Subtitle />
</Sequence>
</AbsoluteFill>
);
}The width, height, fps, and duration of a video is defined in src/Root.tsx:
import { Composition } from "remotion";
import { MyComposition } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
/>
);
};Metadata can also be calculated dynamically:
import { Composition, CalculateMetadataFunction } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
const calculateMetadata: CalculateMetadataFunction<
MyCompositionProps
> = async ({ props, abortSignal }) => {
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
signal: abortSignal,
}).then((res) => res.json());
return {
durationInFrames: Math.ceil(data.duration * 30),
props: {
...props,
videoUrl: data.url,
},
width: 1080,
height: 1080,
};
};
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
fps={30}
width={1080}
height={1080}
defaultProps={{ videoId: "abc123" }}
calculateMetadata={calculateMetadata}
/>
);
};Starting preview
Start the Remotion Studio to preview a video:
npx remotion studioOptional: one-frame render check
You can render a single frame with the CLI to sanity-check layout, colors, or timing. Skip it for trivial edits, pure refactors, or when you already have enough confidence from Studio or prior renders.
npx remotion still [composition-id] --scale=0.25 --frame=30At 30 fps, --frame=30 is the one-second mark (--frame is zero-based).
Captions
When dealing with captions or subtitles, load the ./rules/subtitles.md file for more information.
Using FFmpeg
For some video operations, such as trimming videos or detecting silence, FFmpeg should be used. Load the ./rules/ffmpeg.md file for more information.
Silence detection
When needing to detect and trim silent segments from video or audio files, load the ./rules/silence-detection.md file.
Audio visualization
When needing to visualize audio (spectrum bars, waveforms, bass-reactive effects), load the ./rules/audio-visualization.md file for more information.
Sound effects
When needing to use sound effects, load the ./rules/sfx.md file for more information.
3D content
See rules/3d.md for 3D content in Remotion using Three.js and React Three Fiber.
Advanced audio
See rules/audio.md for advanced audio features like trimming, volume, speed, pitch.
Dynamic duration, dimensions and data
See rules/calculate-metadata.md for dynamically set composition duration, dimensions, and props.
Advanced compositions
See rules/compositions.md for how to define stills, folders, default props and for how to nest compositions.
Google Fonts
Is the recommended way to load fonts in Remotion. See rules/google-fonts.md for how to load Google Fonts.
Local fonts
See rules/local-fonts.md for how to load local fonts.
Getting audio duration
See rules/get-audio-duration.md for getting the duration of an audio file in seconds with Mediabunny.
Getting video dimensions
See rules/get-video-dimensions.md for getting the width and height of a video file with Mediabunny.
Getting video duration
See rules/get-video-duration.md for getting the duration of a video file in seconds with Mediabunny.
GIFs
See rules/gifs.md for how to display GIFs synchronized with Remotion's timeline.
Advanced Images
See rules/images.md for sizing and positioning images, dynamic image paths, and getting image dimensions.
Light leaks
See rules/light-leaks.md for light leak overlay effects using @remotion/light-leaks.
Lottie animations
See rules/lottie.md for embedding Lottie animations in Remotion.
Measuring DOM nodes
See rules/measuring-dom-nodes.md for measuring DOM element dimensions in Remotion.
Measuring text
See rules/measuring-text.md for measuring text dimensions, fitting text to containers, and checking overflow.
Advanced sequencing
See rules/sequencing.md for more sequencing patterns - delay, trim, limit duration of items.
TailwindCSS
See rules/tailwind.md for using TailwindCSS in Remotion.
Text animations
See rules/text-animations.md for typography and text animation patterns.
Advanced timing
See rules/timing.md for advanced timing with interpolate and Bézier easing, and springs.
Transitions
See rules/transitions.md for scene transition patterns.
Transparent videos
See rules/transparent-videos.md for rendering out a video with transparency.
Trimming
See rules/trimming.md for trimming patterns - cutting the beginning or end of animations.
Advanced Videos
See rules/videos.md for advanced knowledge about embedding videos - trimming, volume, speed, looping, pitch.
Parameterized videos
See rules/parameters.md for making a composition parametrizable by adding a Zod schema.
Maps
See rules/maps.md for adding a map using Mapbox and animating it.
Voiceover
See rules/voiceover.md for adding AI-generated voiceover to Remotion compositions using ElevenLabs TTS.
Using Three.js and React Three Fiber in Remotion
Follow React Three Fiber and Three.js best practices. Only the following Remotion-specific rules need to be followed:
Prerequisites
First, the @remotion/three package needs to be installed. If it is not, use the following command:
npx remotion add @remotion/three # If project uses npm
bunx remotion add @remotion/three # If project uses bun
yarn remotion add @remotion/three # If project uses yarn
pnpm exec remotion add @remotion/three # If project uses pnpmUsing ThreeCanvas
You MUST wrap 3D content in <ThreeCanvas> and include proper lighting. <ThreeCanvas> MUST have a width and height prop.
import { ThreeCanvas } from "@remotion/three";
import { useVideoConfig } from "remotion";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={0.8} />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial color="red" />
</mesh>
</ThreeCanvas>;No animations not driven by useCurrentFrame()
Shaders, models etc MUST NOT animate by themselves. No animations are allowed unless they are driven by useCurrentFrame(). Otherwise, it will cause flickering during rendering.
Using useFrame() from @react-three/fiber is forbidden.
Animate using useCurrentFrame()
Use useCurrentFrame() to perform animations.
const frame = useCurrentFrame();
const rotationY = frame * 0.02;
<mesh rotation={[0, rotationY, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>;Using <Sequence> inside <ThreeCanvas>
The layout prop of any <Sequence> inside a <ThreeCanvas> must be set to none.
import { Sequence } from "remotion";
import { ThreeCanvas } from "@remotion/three";
const { width, height } = useVideoConfig();
<ThreeCanvas width={width} height={height}>
<Sequence layout="none">
<mesh>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
</Sequence>
</ThreeCanvas>;import {loadFont} from '@remotion/google-fonts/Inter';
import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion';
const {fontFamily} = loadFont();
const COLOR_BAR = '#D4AF37';
const COLOR_TEXT = '#ffffff';
const COLOR_MUTED = '#888888';
const COLOR_BG = '#0a0a0a';
const COLOR_AXIS = '#333333';
// Ideal composition size: 1280x720
const Title: React.FC<{children: React.ReactNode}> = ({children}) => (
<div style={{textAlign: 'center', marginBottom: 40}}>
<div style={{color: COLOR_TEXT, fontSize: 48, fontWeight: 600}}>
{children}
</div>
</div>
);
const YAxis: React.FC<{steps: number[]; height: number}> = ({
steps,
height,
}) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
height,
paddingRight: 16,
}}
>
{steps
.slice()
.reverse()
.map((step) => (
<div
key={step}
style={{
color: COLOR_MUTED,
fontSize: 20,
textAlign: 'right',
}}
>
{step.toLocaleString()}
</div>
))}
</div>
);
const Bar: React.FC<{
height: number;
progress: number;
}> = ({height, progress}) => (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
}}
>
<div
style={{
width: '100%',
height,
backgroundColor: COLOR_BAR,
borderRadius: '8px 8px 0 0',
opacity: progress,
}}
/>
</div>
);
const XAxis: React.FC<{
children: React.ReactNode;
labels: string[];
height: number;
}> = ({children, labels, height}) => (
<div style={{flex: 1, display: 'flex', flexDirection: 'column'}}>
<div
style={{
display: 'flex',
alignItems: 'flex-end',
gap: 16,
height,
borderLeft: `2px solid ${COLOR_AXIS}`,
borderBottom: `2px solid ${COLOR_AXIS}`,
paddingLeft: 16,
}}
>
{children}
</div>
<div
style={{
display: 'flex',
gap: 16,
paddingLeft: 16,
marginTop: 12,
}}
>
{labels.map((label) => (
<div
key={label}
style={{
flex: 1,
textAlign: 'center',
color: COLOR_MUTED,
fontSize: 20,
}}
>
{label}
</div>
))}
</div>
</div>
);
export const MyAnimation = () => {
const frame = useCurrentFrame();
const {fps, height} = useVideoConfig();
const data = [
{month: 'Jan', price: 2039},
{month: 'Mar', price: 2160},
{month: 'May', price: 2327},
{month: 'Jul', price: 2426},
{month: 'Sep', price: 2634},
{month: 'Nov', price: 2672},
];
const minPrice = 2000;
const maxPrice = 2800;
const priceRange = maxPrice - minPrice;
const chartHeight = height - 280;
const yAxisSteps = [2000, 2400, 2800];
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
padding: 60,
display: 'flex',
flexDirection: 'column',
fontFamily,
}}
>
<Title>Gold Price 2024</Title>
<div style={{display: 'flex', flex: 1}}>
<YAxis steps={yAxisSteps} height={chartHeight} />
<XAxis height={chartHeight} labels={data.map((d) => d.month)}>
{data.map((item, i) => {
const progress = spring({
frame: frame - i * 5 - 10,
fps,
config: {damping: 18, stiffness: 80},
});
const barHeight =
((item.price - minPrice) / priceRange) * chartHeight * progress;
return (
<Bar key={item.month} height={barHeight} progress={progress} />
);
})}
</XAxis>
</div>
</AbsoluteFill>
);
};
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
const COLOR_BG = '#ffffff';
const COLOR_TEXT = '#000000';
const FULL_TEXT = 'From prompt to motion graphics. This is Remotion.';
const PAUSE_AFTER = 'From prompt to motion graphics.';
const FONT_SIZE = 72;
const FONT_WEIGHT = 700;
const CHAR_FRAMES = 2;
const CURSOR_BLINK_FRAMES = 16;
const PAUSE_SECONDS = 1;
// Ideal composition size: 1280x720
const getTypedText = ({
frame,
fullText,
pauseAfter,
charFrames,
pauseFrames,
}: {
frame: number;
fullText: string;
pauseAfter: string;
charFrames: number;
pauseFrames: number;
}): string => {
const pauseIndex = fullText.indexOf(pauseAfter);
const preLen =
pauseIndex >= 0 ? pauseIndex + pauseAfter.length : fullText.length;
let typedChars = 0;
if (frame < preLen * charFrames) {
typedChars = Math.floor(frame / charFrames);
} else if (frame < preLen * charFrames + pauseFrames) {
typedChars = preLen;
} else {
const postPhase = frame - preLen * charFrames - pauseFrames;
typedChars = Math.min(
fullText.length,
preLen + Math.floor(postPhase / charFrames),
);
}
return fullText.slice(0, typedChars);
};
const Cursor: React.FC<{
frame: number;
blinkFrames: number;
symbol?: string;
}> = ({frame, blinkFrames, symbol = '\u258C'}) => {
const opacity = interpolate(
frame % blinkFrames,
[0, blinkFrames / 2, blinkFrames],
[1, 0, 1],
{extrapolateLeft: 'clamp', extrapolateRight: 'clamp'},
);
return <span style={{opacity}}>{symbol}</span>;
};
export const MyAnimation = () => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const pauseFrames = Math.round(fps * PAUSE_SECONDS);
const typedText = getTypedText({
frame,
fullText: FULL_TEXT,
pauseAfter: PAUSE_AFTER,
charFrames: CHAR_FRAMES,
pauseFrames,
});
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
}}
>
<div
style={{
color: COLOR_TEXT,
fontSize: FONT_SIZE,
fontWeight: FONT_WEIGHT,
fontFamily: 'sans-serif',
}}
>
<span>{typedText}</span>
<Cursor frame={frame} blinkFrames={CURSOR_BLINK_FRAMES} />
</div>
</AbsoluteFill>
);
};
import {loadFont} from '@remotion/google-fonts/Inter';
import React from 'react';
import {AbsoluteFill, spring, useCurrentFrame, useVideoConfig} from 'remotion';
/*
* Highlight a word in a sentence with a spring-animated wipe effect.
*/
// Ideal composition size: 1280x720
const COLOR_BG = '#ffffff';
const COLOR_TEXT = '#000000';
const COLOR_HIGHLIGHT = '#A7C7E7';
const FULL_TEXT = 'This is Remotion.';
const HIGHLIGHT_WORD = 'Remotion';
const FONT_SIZE = 72;
const FONT_WEIGHT = 700;
const HIGHLIGHT_START_FRAME = 30;
const HIGHLIGHT_WIPE_DURATION = 18;
const {fontFamily} = loadFont();
const Highlight: React.FC<{
word: string;
color: string;
delay: number;
durationInFrames: number;
}> = ({word, color, delay, durationInFrames}) => {
const frame = useCurrentFrame();
const {fps} = useVideoConfig();
const highlightProgress = spring({
fps,
frame,
config: {damping: 200},
delay,
durationInFrames,
});
const scaleX = Math.max(0, Math.min(1, highlightProgress));
return (
<span style={{position: 'relative', display: 'inline-block'}}>
<span
style={{
position: 'absolute',
left: 0,
right: 0,
top: '50%',
height: '1.05em',
transform: `translateY(-50%) scaleX(${scaleX})`,
transformOrigin: 'left center',
backgroundColor: color,
borderRadius: '0.18em',
zIndex: 0,
}}
/>
<span style={{position: 'relative', zIndex: 1}}>{word}</span>
</span>
);
};
export const MyAnimation = () => {
const highlightIndex = FULL_TEXT.indexOf(HIGHLIGHT_WORD);
const hasHighlight = highlightIndex >= 0;
const preText = hasHighlight ? FULL_TEXT.slice(0, highlightIndex) : FULL_TEXT;
const postText = hasHighlight
? FULL_TEXT.slice(highlightIndex + HIGHLIGHT_WORD.length)
: '';
return (
<AbsoluteFill
style={{
backgroundColor: COLOR_BG,
alignItems: 'center',
justifyContent: 'center',
fontFamily,
}}
>
<div
style={{
color: COLOR_TEXT,
fontSize: FONT_SIZE,
fontWeight: FONT_WEIGHT,
}}
>
{hasHighlight ? (
<>
<span>{preText}</span>
<Highlight
word={HIGHLIGHT_WORD}
color={COLOR_HIGHLIGHT}
delay={HIGHLIGHT_START_FRAME}
durationInFrames={HIGHLIGHT_WIPE_DURATION}
/>
<span>{postText}</span>
</>
) : (
<span>{FULL_TEXT}</span>
)}
</div>
</AbsoluteFill>
);
};
Audio Visualization in Remotion
Prerequisites
npx remotion add @remotion/media-utilsLoading Audio Data
Use useWindowedAudioData() (https://www.remotion.dev/docs/use-windowed-audio-data) to load audio data:
import { useWindowedAudioData } from "@remotion/media-utils";
import { staticFile, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { audioData, dataOffsetInSeconds } = useWindowedAudioData({
src: staticFile("podcast.wav"),
frame,
fps,
windowInSeconds: 30,
});Spectrum Bar Visualization
Use visualizeAudio() (https://www.remotion.dev/docs/visualize-audio) to get frequency data for bar charts:
import { useWindowedAudioData, visualizeAudio } from "@remotion/media-utils";
import { staticFile, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { audioData, dataOffsetInSeconds } = useWindowedAudioData({
src: staticFile("music.mp3"),
frame,
fps,
windowInSeconds: 30,
});
if (!audioData) {
return null;
}
const frequencies = visualizeAudio({
fps,
frame,
audioData,
numberOfSamples: 256,
optimizeFor: "speed",
dataOffsetInSeconds,
});
return (
<div style={{ display: "flex", alignItems: "flex-end", height: 200 }}>
{frequencies.map((v, i) => (
<div
key={i}
style={{
flex: 1,
height: `${v * 100}%`,
backgroundColor: "#0b84f3",
margin: "0 1px",
}}
/>
))}
</div>
);numberOfSamplesmust be power of 2 (32, 64, 128, 256, 512, 1024)- Values range 0-1; left of array = bass, right = highs
- Use
optimizeFor: "speed"for Lambda or high sample counts
Important: When passing audioData to child components, also pass the frame from the parent. Do not call useCurrentFrame() in each child - this causes discontinuous visualization when children are inside <Sequence> with offsets.
Waveform Visualization
Use visualizeAudioWaveform() (https://www.remotion.dev/docs/media-utils/visualize-audio-waveform) with createSmoothSvgPath() (https://www.remotion.dev/docs/media-utils/create-smooth-svg-path) for oscilloscope-style displays:
import {
createSmoothSvgPath,
useWindowedAudioData,
visualizeAudioWaveform,
} from "@remotion/media-utils";
import { staticFile, useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { width, fps } = useVideoConfig();
const HEIGHT = 200;
const { audioData, dataOffsetInSeconds } = useWindowedAudioData({
src: staticFile("voice.wav"),
frame,
fps,
windowInSeconds: 30,
});
if (!audioData) {
return null;
}
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: HEIGHT / 2 + (y * HEIGHT) / 2,
})),
});
return (
<svg width={width} height={HEIGHT}>
<path d={path} fill="none" stroke="#0b84f3" strokeWidth={2} />
</svg>
);Bass-Reactive Effects
Extract low frequencies for beat-reactive animations:
const frequencies = visualizeAudio({
fps,
frame,
audioData,
numberOfSamples: 128,
optimizeFor: "speed",
dataOffsetInSeconds,
});
const lowFrequencies = frequencies.slice(0, 32);
const bassIntensity =
lowFrequencies.reduce((sum, v) => sum + v, 0) / lowFrequencies.length;
const scale = 1 + bassIntensity * 0.5;
const opacity = Math.min(0.6, bassIntensity * 0.8);Volume-Based Waveform
Use getWaveformPortion() (https://www.remotion.dev/docs/get-waveform-portion) when you need simplified volume data instead of frequency spectrum:
import { getWaveformPortion } from "@remotion/media-utils";
import { useCurrentFrame, useVideoConfig } from "remotion";
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const currentTimeInSeconds = frame / fps;
const waveform = getWaveformPortion({
audioData,
startTimeInSeconds: currentTimeInSeconds,
durationInSeconds: 5,
numberOfSamples: 50,
});
// Returns array of { index, amplitude } objects (amplitude: 0-1)
waveform.map((bar) => (
<div key={bar.index} style={{ height: bar.amplitude * 100 }} />
));Postprocessing
Low frequencies naturally dominate. Apply logarithmic scaling for visual balance:
const minDb = -100;
const maxDb = -30;
const scaled = frequencies.map((value) => {
const db = 20 * Math.log10(value);
return (db - minDb) / (maxDb - minDb);
});Using audio in Remotion
Prerequisites
First, the @remotion/media package needs to be installed. If it is not installed, use the following command:
npx remotion add @remotion/mediaImporting Audio
Use <Audio> from @remotion/media to add audio to your composition.
import { Audio } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Audio src={staticFile("audio.mp3")} />;
};Remote URLs are also supported:
<Audio src="https://remotion.media/audio.mp3" />By default, audio plays from the start, at full volume and full length. Multiple audio tracks can be layered by adding multiple <Audio> components.
Trimming
Use trimBefore and trimAfter to remove portions of the audio. Values are in frames.
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
trimBefore={2 * fps} // Skip the first 2 seconds
trimAfter={10 * fps} // End at the 10 second mark
/>
);The audio still starts playing at the beginning of the composition - only the specified portion is played.
Delaying
Wrap the audio in a <Sequence> to delay when it starts:
import { Sequence, staticFile } from "remotion";
import { Audio } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Audio src={staticFile("audio.mp3")} />
</Sequence>
);The audio will start playing after 1 second.
Volume
Set a static volume (0 to 1):
<Audio src={staticFile("audio.mp3")} volume={0.5} />Or use a callback for dynamic volume based on the current frame:
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);The value of f starts at 0 when the audio begins to play, not the composition frame.
Muting
Use muted to silence the audio. It can be set dynamically:
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<Audio
src={staticFile("audio.mp3")}
muted={frame >= 2 * fps && frame <= 4 * fps} // Mute between 2s and 4s
/>
);Speed
Use playbackRate to change the playback speed:
<Audio src={staticFile("audio.mp3")} playbackRate={2} /> {/* 2x speed */}
<Audio src={staticFile("audio.mp3")} playbackRate={0.5} /> {/* Half speed */}Reverse playback is not supported.
Looping
Use loop to loop the audio indefinitely:
<Audio src={staticFile("audio.mp3")} loop />Use loopVolumeCurveBehavior to control how the frame count behaves when looping:
"repeat": Frame count resets to 0 each loop (default)"extend": Frame count continues incrementing
<Audio
src={staticFile("audio.mp3")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
/>Pitch
Use toneFrequency to adjust the pitch without affecting speed. Values range from 0.01 to 2:
<Audio
src={staticFile("audio.mp3")}
toneFrequency={1.5} // Higher pitch
/>
<Audio
src={staticFile("audio.mp3")}
toneFrequency={0.8} // Lower pitch
/>Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the <Player />.
Using calculateMetadata
Use calculateMetadata on a <Composition> to dynamically set duration, dimensions, and transform props before rendering.
<Composition
id="MyComp"
component={MyComponent}
durationInFrames={300}
fps={30}
width={1920}
height={1080}
defaultProps={{ videoSrc: "https://remotion.media/video.mp4" }}
calculateMetadata={calculateMetadata}
/>Setting duration based on a video
Use the `getVideoDuration` and `getVideoDimensions` skills to get the video duration and dimensions:
import { CalculateMetadataFunction } from "remotion";
import { getVideoDuration } from "./get-video-duration";
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
const durationInSeconds = await getVideoDuration(props.videoSrc);
return {
durationInFrames: Math.ceil(durationInSeconds * 30),
};
};Matching dimensions of a video
Use the `getVideoDimensions` skill to get the video dimensions:
import { CalculateMetadataFunction } from "remotion";
import { getVideoDuration } from "./get-video-duration";
import { getVideoDimensions } from "./get-video-dimensions";
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
const dimensions = await getVideoDimensions(props.videoSrc);
return {
width: dimensions.width,
height: dimensions.height,
};
};Setting duration based on multiple videos
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
const metadataPromises = props.videos.map((video) =>
getVideoDuration(video.src),
);
const allMetadata = await Promise.all(metadataPromises);
const totalDuration = allMetadata.reduce(
(sum, durationInSeconds) => sum + durationInSeconds,
0,
);
return {
durationInFrames: Math.ceil(totalDuration * 30),
};
};Setting a default outName
Set the default output filename based on props:
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
return {
defaultOutName: `video-${props.id}`, // .mp4 is added automatically
};
};Transforming props
Fetch data or transform props before rendering:
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
abortSignal,
}) => {
const response = await fetch(props.dataUrl, { signal: abortSignal });
const data = await response.json();
return {
props: {
...props,
fetchedData: data,
},
};
};The abortSignal cancels stale requests when props change in the Studio.
Return value
All fields are optional. Returned values override the <Composition> props:
durationInFrames: Number of frameswidth: Composition width in pixelsheight: Composition height in pixelsfps: Frames per secondprops: Transformed props passed to the componentdefaultOutName: Default output filenamedefaultCodec: Default codec for rendering
A <Composition> defines the component, width, height, fps and duration of a renderable video.
Default Props
Pass defaultProps to provide initial values for your component. Values must be JSON-serializable (Date, Map, Set, and staticFile() are supported).
import { Composition } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
defaultProps={
{
title: "Hello World",
color: "#ff0000",
} satisfies MyCompositionProps
}
/>
);
};Use type declarations for props rather than interface to ensure defaultProps type safety.
Folders
Use <Folder> to organize compositions in the sidebar. Folder names can only contain letters, numbers, and hyphens.
import { Composition, Folder } from "remotion";
export const RemotionRoot = () => {
return (
<>
<Folder name="Marketing">
<Composition id="Promo" /* ... */ />
<Composition id="Ad" /* ... */ />
</Folder>
<Folder name="Social">
<Folder name="Instagram">
<Composition id="Story" /* ... */ />
<Composition id="Reel" /* ... */ />
</Folder>
</Folder>
</>
);
};Stills
Use <Still> for single-frame images. It does not require durationInFrames or fps.
import { Still } from "remotion";
import { Thumbnail } from "./Thumbnail";
export const RemotionRoot = () => {
return (
<Still id="Thumbnail" component={Thumbnail} width={1280} height={720} />
);
};Calculate Metadata
Use calculateMetadata to make dimensions, duration, or props dynamic based on data.
import { Composition, CalculateMetadataFunction } from "remotion";
import { MyComposition, MyCompositionProps } from "./MyComposition";
const calculateMetadata: CalculateMetadataFunction<
MyCompositionProps
> = async ({ props, abortSignal }) => {
const data = await fetch(`https://api.example.com/video/${props.videoId}`, {
signal: abortSignal,
}).then((res) => res.json());
return {
durationInFrames: Math.ceil(data.duration * 30),
props: {
...props,
videoUrl: data.url,
},
};
};
export const RemotionRoot = () => {
return (
<Composition
id="MyComposition"
component={MyComposition}
fps={30}
width={1080}
height={1080}
defaultProps={{ videoId: "abc123" }}
calculateMetadata={calculateMetadata}
/>
);
};The function can return props, durationInFrames, width, height, fps, and codec-related defaults. It runs once before rendering begins.
Nesting compositions within another
To add a composition within another composition, you can use the <Sequence> component with a width and height prop to specify the size of the composition.
<AbsoluteFill>
<Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>
<CompositionComponent />
</Sequence>
</AbsoluteFill>Displaying captions in Remotion
This guide explains how to display captions in Remotion, assuming you already have captions in the `Caption` format.
Prerequisites
Read Transcribing audio for how to generate captions.
First, the `@remotion/captions` package needs to be installed. If it is not installed, use the following command:
npx remotion add @remotion/captionsFetching captions
First, fetch your captions JSON file. Use `useDelayRender()` to hold the render until the captions are loaded:
import { useState, useEffect, useCallback } from "react";
import { AbsoluteFill, staticFile, useDelayRender } from "remotion";
import type { Caption } from "@remotion/captions";
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const { delayRender, continueRender, cancelRender } = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
// Assuming captions.json is in the public/ folder.
const response = await fetch(staticFile("captions123.json"));
const data = await response.json();
setCaptions(data);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* Render captions here */}</AbsoluteFill>;
};Creating pages
Use createTikTokStyleCaptions() to group captions into pages. The combineTokensWithinMilliseconds option controls how many words appear at once:
import { useMemo } from "react";
import { createTikTokStyleCaptions } from "@remotion/captions";
import type { Caption } from "@remotion/captions";
// How often captions should switch (in milliseconds)
// Higher values = more words per page
// Lower values = fewer words (more word-by-word)
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const { pages } = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
});
}, [captions]);Rendering with Sequences
Map over the pages and render each one in a <Sequence>. Calculate the start frame and duration from the page timing:
import { Sequence, useVideoConfig, AbsoluteFill } from "remotion";
import type { TikTokPage } from "@remotion/captions";
const CaptionedContent: React.FC = () => {
const { fps } = useVideoConfig();
return (
<AbsoluteFill>
{pages.map((page, index) => {
const nextPage = pages[index + 1] ?? null;
const startFrame = (page.startMs / 1000) * fps;
const endFrame = Math.min(
nextPage ? (nextPage.startMs / 1000) * fps : Infinity,
startFrame + (SWITCH_CAPTIONS_EVERY_MS / 1000) * fps,
);
const durationInFrames = endFrame - startFrame;
if (durationInFrames <= 0) {
return null;
}
return (
<Sequence
key={index}
from={startFrame}
durationInFrames={durationInFrames}
>
<CaptionPage page={page} />
</Sequence>
);
})}
</AbsoluteFill>
);
};White-space preservation
The captions are whitespace sensitive. You should include spaces in the text field before each word. Use whiteSpace: "pre" to preserve the whitespace in the captions.
Separate component for captions
Put captioning logic in a separate component. Make a new file for it.
Word highlighting
A caption page contains tokens which you can use to highlight the currently spoken word:
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from "remotion";
import type { TikTokPage } from "@remotion/captions";
const HIGHLIGHT_COLOR = "#39E508";
const CaptionPage: React.FC<{ page: TikTokPage }> = ({ page }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Current time relative to the start of the sequence
const currentTimeMs = (frame / fps) * 1000;
// Convert to absolute time by adding the page start
const absoluteTimeMs = page.startMs + currentTimeMs;
return (
<AbsoluteFill style={{ justifyContent: "center", alignItems: "center" }}>
<div style={{ fontSize: 80, fontWeight: "bold", whiteSpace: "pre" }}>
{page.tokens.map((token) => {
const isActive =
token.fromMs <= absoluteTimeMs && token.toMs > absoluteTimeMs;
return (
<span
key={token.fromMs}
style={{ color: isActive ? HIGHLIGHT_COLOR : "white" }}
>
{token.text}
</span>
);
})}
</div>
</AbsoluteFill>
);
};Display captions alongside video content
By default, put the captions alongside the video content, so the captions are in sync. For each video, make a new captions JSON file.
<AbsoluteFill>
<Video src={staticFile("video.mp4")} />
<CaptionPage page={page} />
</AbsoluteFill>FFmpeg in Remotion
ffmpeg and ffprobe do not need to be installed. They are available via the npx remotion ffmpeg and npx remotion ffprobe:
npx remotion ffmpeg -i input.mp4 output.mp3
npx remotion ffprobe input.mp4Trimming videos
You have 2 options for trimming videos:
1. Preferred: Use the trimBefore and trimAfter props of the <Video> component. This is non-destructive, requires no re-encoding, and you can change the trim at any time.
import {Video} from '@remotion/media';
<Video src={staticFile('video.mp4')} trimBefore={5 * fps} trimAfter={10 * fps} />;2. Use the FFmpeg command line. You MUST re-encode the video to avoid frozen frames at the start of the video. Only use this if you need a standalone trimmed file (e.g. for upload or external use).
# Re-encodes from the exact frame
npx remotion ffmpeg -ss 00:00:05 -i public/input.mp4 -to 00:00:10 -c:v libx264 -c:a aac public/output.mp4Getting audio duration with Mediabunny
Mediabunny can extract the duration of an audio file. It works in browser, Node.js, and Bun environments.
Getting audio duration
```tsx title="get-audio-duration.ts" import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getAudioDuration = async (src: string) => { const input = new Input({ formats: ALL_FORMATS, source: new UrlSource(src, { getRetryDelay: () => null, }), });
const durationInSeconds = await input.computeDuration(); return durationInSeconds; };
## Usage
const duration = await getAudioDuration("https://remotion.media/audio.mp3"); console.log(duration); // e.g. 180.5 (seconds)
## Using with staticFile in Remotion
Make sure to wrap the file path in `staticFile()`:
import { staticFile } from "remotion";
const duration = await getAudioDuration(staticFile("audio.mp3"));
## In Node.js and Bun
Use `FileSource` instead of `UrlSource`:
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({ formats: ALL_FORMATS, source: new FileSource(file), // File object from input or drag-drop });
Getting video dimensions with Mediabunny
Mediabunny can extract the width and height of a video file. It works in browser, Node.js, and Bun environments.
Getting video dimensions
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDimensions = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const videoTrack = await input.getPrimaryVideoTrack();
if (!videoTrack) {
throw new Error("No video track found");
}
return {
width: videoTrack.displayWidth,
height: videoTrack.displayHeight,
};
};Usage
const dimensions = await getVideoDimensions("https://remotion.media/video.mp4");
console.log(dimensions.width); // e.g. 1920
console.log(dimensions.height); // e.g. 1080Using with local files
For local files, use FileSource instead of UrlSource:
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const videoTrack = await input.getPrimaryVideoTrack();
const width = videoTrack.displayWidth;
const height = videoTrack.displayHeight;Using with staticFile in Remotion
import { staticFile } from "remotion";
const dimensions = await getVideoDimensions(staticFile("video.mp4"));Getting video duration with Mediabunny
Mediabunny can extract the duration of a video file. It works in browser, Node.js, and Bun environments.
Getting video duration
import { Input, ALL_FORMATS, UrlSource } from "mediabunny";
export const getVideoDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, {
getRetryDelay: () => null,
}),
});
const durationInSeconds = await input.computeDuration();
return durationInSeconds;
};Usage
const duration = await getVideoDuration("https://remotion.media/video.mp4");
console.log(duration); // e.g. 10.5 (seconds)Video files from the public/ directory
Make sure to wrap the file path in staticFile():
import { staticFile } from "remotion";
const duration = await getVideoDuration(staticFile("video.mp4"));In Node.js and Bun
Use FileSource instead of UrlSource:
import { Input, ALL_FORMATS, FileSource } from "mediabunny";
const input = new Input({
formats: ALL_FORMATS,
source: new FileSource(file), // File object from input or drag-drop
});
const durationInSeconds = await input.computeDuration();Using Animated images in Remotion
Basic usage
Use <AnimatedImage> to display a GIF, APNG, AVIF or WebP image synchronized with Remotion's timeline:
import { AnimatedImage, staticFile } from "remotion";
export const MyComposition = () => {
return (
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} />
);
};Remote URLs are also supported (must have CORS enabled):
<AnimatedImage
src="https://example.com/animation.gif"
width={500}
height={500}
/>Sizing and fit
Control how the image fills its container with the fit prop:
// Stretch to fill (default)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="fill" />
// Maintain aspect ratio, fit inside container
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="contain" />
// Fill container, crop if needed
<AnimatedImage src={staticFile("animation.gif")} width={500} height={300} fit="cover" />Playback speed
Use playbackRate to control the animation speed:
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={2} /> {/* 2x speed */}
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} playbackRate={0.5} /> {/* Half speed */}Looping behavior
Control what happens when the animation finishes:
// Loop indefinitely (default)
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="loop" />
// Play once, show final frame
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="pause-after-finish" />
// Play once, then clear canvas
<AnimatedImage src={staticFile("animation.gif")} width={500} height={500} loopBehavior="clear-after-finish" />Styling
Use the style prop for additional CSS (use width and height props for sizing):
<AnimatedImage
src={staticFile("animation.gif")}
width={500}
height={500}
style={{
borderRadius: 20,
position: "absolute",
top: 100,
left: 50,
}}
/>Getting GIF duration
Use getGifDurationInSeconds() from @remotion/gif to get the duration of a GIF.
npx remotion add @remotion/gifimport { getGifDurationInSeconds } from "@remotion/gif";
import { staticFile } from "remotion";
const duration = await getGifDurationInSeconds(staticFile("animation.gif"));
console.log(duration); // e.g. 2.5This is useful for setting the composition duration to match the GIF:
import { getGifDurationInSeconds } from "@remotion/gif";
import { staticFile, CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction = async () => {
const duration = await getGifDurationInSeconds(staticFile("animation.gif"));
return {
durationInFrames: Math.ceil(duration * 30),
};
};Alternative
If <AnimatedImage> does not work (only supported in Chrome and Firefox), you can use <Gif> from @remotion/gif instead.
npx remotion add @remotion/gif # If project uses npm
bunx remotion add @remotion/gif # If project uses bun
yarn remotion add @remotion/gif # If project uses yarn
pnpm exec remotion add @remotion/gif # If project uses pnpmimport { Gif } from "@remotion/gif";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Gif src={staticFile("animation.gif")} width={500} height={500} />;
};The <Gif> component has the same props as <AnimatedImage> but only supports GIF files.
Using fonts in Remotion
Google Fonts with @remotion/google-fonts
The recommended way to use Google Fonts. It's type-safe and automatically blocks rendering until the font is ready.
Prerequisites
First, the @remotion/google-fonts package needs to be installed. If it is not installed, use the following command:
npx remotion add @remotion/google-fonts # If project uses npm
bunx remotion add @remotion/google-fonts # If project uses bun
yarn remotion add @remotion/google-fonts # If project uses yarn
pnpm exec remotion add @remotion/google-fonts # If project uses pnpmimport { loadFont } from "@remotion/google-fonts/Lobster";
const { fontFamily } = loadFont();
export const MyComposition = () => {
return <div style={{ fontFamily }}>Hello World</div>;
};Preferrably, specify only needed weights and subsets to reduce file size:
import { loadFont } from "@remotion/google-fonts/Roboto";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});Using in components
Call loadFont() at the top level of your component or in a separate file that's imported early:
import { loadFont } from "@remotion/google-fonts/Montserrat";
const { fontFamily } = loadFont("normal", {
weights: ["400", "700"],
subsets: ["latin"],
});
export const Title: React.FC<{ text: string }> = ({ text }) => {
return (
<h1
style={{
fontFamily,
fontSize: 80,
fontWeight: "bold",
}}
>
{text}
</h1>
);
};Sizing and positioning
Use the style prop to control size and position:
<Img
src={staticFile("photo.png")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>Dynamic image paths
Use template literals for dynamic file references:
import { Img, staticFile, useCurrentFrame } from "remotion";
const frame = useCurrentFrame();
// Image sequence
<Img src={staticFile(`frames/frame${frame}.png`)} />
// Selecting based on props
<Img src={staticFile(`avatars/${props.userId}.png`)} />
// Conditional images
<Img src={staticFile(`icons/${isActive ? "active" : "inactive"}.svg`)} />This pattern is useful for:
- Image sequences (frame-by-frame animations)
- User-specific avatars or profile images
- Theme-based icons
- State-dependent graphics
Getting image dimensions
Use getImageDimensions() to get the dimensions of an image:
import { getImageDimensions, staticFile } from "remotion";
const { width, height } = await getImageDimensions(staticFile("photo.png"));This is useful for calculating aspect ratios or sizing compositions:
import {
getImageDimensions,
staticFile,
CalculateMetadataFunction,
} from "remotion";
const calculateMetadata: CalculateMetadataFunction = async () => {
const { width, height } = await getImageDimensions(staticFile("photo.png"));
return {
width,
height,
};
};Importing .srt subtitles into Remotion
If you have an existing .srt subtitle file, you can import it into Remotion using parseSrt() from @remotion/captions.
If you don't have a .srt file, read Transcribing audio for how to generate captions instead.
Prerequisites
First, the @remotion/captions package needs to be installed. If it is not installed, use the following command:
npx remotion add @remotion/captions # If project uses npm
bunx remotion add @remotion/captions # If project uses bun
yarn remotion add @remotion/captions # If project uses yarn
pnpm exec remotion add @remotion/captions # If project uses pnpmReading an .srt file
Use staticFile() to reference an .srt file in your public folder, then fetch and parse it:
import { useState, useEffect, useCallback } from "react";
import { AbsoluteFill, staticFile, useDelayRender } from "remotion";
import { parseSrt } from "@remotion/captions";
import type { Caption } from "@remotion/captions";
export const MyComponent: React.FC = () => {
const [captions, setCaptions] = useState<Caption[] | null>(null);
const { delayRender, continueRender, cancelRender } = useDelayRender();
const [handle] = useState(() => delayRender());
const fetchCaptions = useCallback(async () => {
try {
const response = await fetch(staticFile("subtitles.srt"));
const text = await response.text();
const { captions: parsed } = parseSrt({ input: text });
setCaptions(parsed);
continueRender(handle);
} catch (e) {
cancelRender(e);
}
}, [continueRender, cancelRender, handle]);
useEffect(() => {
fetchCaptions();
}, [fetchCaptions]);
if (!captions) {
return null;
}
return <AbsoluteFill>{/* Use captions here */}</AbsoluteFill>;
};Remote URLs are also supported - you can fetch() a remote file via URL instead of using staticFile().
Using imported captions
Once parsed, the captions are in the Caption format and can be used with all @remotion/captions utilities.
Light Leaks
This only works from Remotion 4.0.415 and up. Use npx remotion versions to check your Remotion version and npx remotion upgrade to upgrade your Remotion version.
<LightLeak> from @remotion/light-leaks renders a WebGL-based light leak effect. It reveals during the first half of its duration and retracts during the second half.
Typically used inside a <TransitionSeries.Overlay> to play over the cut point between two scenes. See the transitions rule for <TransitionSeries> and overlay usage.
Prerequisites
npx remotion add @remotion/light-leaksBasic usage with TransitionSeries
import { TransitionSeries } from "@remotion/transitions";
import { LightLeak } from "@remotion/light-leaks";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Overlay durationInFrames={30}>
<LightLeak />
</TransitionSeries.Overlay>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>;Props
durationInFrames?— defaults to the parent sequence/composition duration. The effect reveals during the first half and retracts during the second half.seed?— determines the shape of the light leak pattern. Different seeds produce different patterns. Default:0.hueShift?— rotates the hue in degrees (0–360). Default:0(yellow-to-orange).120= green,240= blue.
Customizing the look
import { LightLeak } from "@remotion/light-leaks";
// Blue-tinted light leak with a different pattern
<LightLeak seed={5} hueShift={240} />;
// Green-tinted light leak
<LightLeak seed={2} hueShift={120} />;Standalone usage
<LightLeak> can also be used outside of <TransitionSeries>, for example as a decorative overlay in any composition:
import { AbsoluteFill } from "remotion";
import { LightLeak } from "@remotion/light-leaks";
const MyComp: React.FC = () => (
<AbsoluteFill>
<MyContent />
<LightLeak durationInFrames={60} seed={3} />
</AbsoluteFill>
);For local font files, use the @remotion/fonts package.
Prerequisites
First, install @remotion/fonts:
npx remotion add @remotion/fonts # If project uses npm
bunx remotion add @remotion/fonts # If project uses bun
yarn remotion add @remotion/fonts # If project uses yarn
pnpm exec remotion add @remotion/fonts # If project uses pnpmLoading a local font
Place your font file in the public/ folder and use loadFont():
import { loadFont } from "@remotion/fonts";
import { staticFile } from "remotion";
await loadFont({
family: "MyFont",
url: staticFile("MyFont-Regular.woff2"),
});
export const MyComposition = () => {
return <div style={{ fontFamily: "MyFont" }}>Hello World</div>;
};Loading multiple weights
Load each weight separately with the same family name:
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",
}),
]);Available options
loadFont({
family: "MyFont", // Required: name to use in CSS
url: staticFile("font.woff2"), // Required: font file URL
format: "woff2", // Optional: auto-detected from extension
weight: "400", // Optional: font weight
style: "normal", // Optional: normal or italic
display: "block", // Optional: font-display behavior
});Using Lottie Animations in Remotion
Prerequisites
First, the @remotion/lottie package needs to be installed. If it is not, use the following command:
npx remotion add @remotion/lottie # If project uses npm
bunx remotion add @remotion/lottie # If project uses bun
yarn remotion add @remotion/lottie # If project uses yarn
pnpm exec remotion add @remotion/lottie # If project uses pnpmDisplaying a Lottie file
To import a Lottie animation:
- Fetch the Lottie asset
- Wrap the loading process in
delayRender()andcontinueRender() - Save the animation data in a state
- Render the Lottie animation using the
Lottiecomponent from the@remotion/lottiepackage
import { Lottie, LottieAnimationData } from "@remotion/lottie";
import { useEffect, useState } from "react";
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((data) => data.json())
.then((json) => {
setAnimationData(json);
continueRender(handle);
})
.catch((err) => {
cancelRender(err);
});
}, [handle]);
if (!animationData) {
return null;
}
return <Lottie animationData={animationData} />;
};Styling and animating
Lottie supports the style prop to allow styles and animations:
return (
<Lottie animationData={animationData} style={{ width: 400, height: 400 }} />
);Maps can be added to a Remotion video with Mapbox. The Mapbox documentation has the API reference.
Prerequisites
Mapbox and @turf/turf need to be installed.
Search the project for lockfiles and run the correct command depending on the package manager:
If package-lock.json is found, use the following command:
npm i mapbox-gl @turf/turf @types/mapbox-glIf bun.lock is found, use the following command:
bun i mapbox-gl @turf/turf @types/mapbox-glIf yarn.lock is found, use the following command:
yarn add mapbox-gl @turf/turf @types/mapbox-glIf pnpm-lock.yaml is found, use the following command:
pnpm i mapbox-gl @turf/turf @types/mapbox-glThe user needs to create a free Mapbox account and create an access token by visiting https://console.mapbox.com/account/access-tokens/.
The mapbox token needs to be added to the .env file:
```txt title=".env" REMOTION_MAPBOX_TOKEN==pk.your-mapbox-access-token
## Adding a map
Here is a basic example of a map in Remotion.
import { useEffect, useMemo, useRef, useState } from "react"; import { AbsoluteFill, useDelayRender, useVideoConfig } from "remotion"; import mapboxgl, { Map } from "mapbox-gl";
export const lineCoordinates = [ [6.56158447265625, 46.059891147620725], [6.5691375732421875, 46.05679376154153], [6.5842437744140625, 46.05059898938315], [6.594886779785156, 46.04702502069337], [6.601066589355469, 46.0460718554722], [6.6089630126953125, 46.0365370783104], [6.6185760498046875, 46.018420689207964], ];
mapboxgl.accessToken = process.env.REMOTION_MAPBOX_TOKEN as string;
export const MyComposition = () => { const ref = useRef<HTMLDivElement>(null); const { delayRender, continueRender } = useDelayRender();
const { width, height } = useVideoConfig(); const [handle] = useState(() => delayRender("Loading map...")); const [map, setMap] = useState<Map | null>(null);
useEffect(() => { const _map = new Map({ container: ref.current!, zoom: 11.53, center: [6.5615, 46.0598], pitch: 65, bearing: 0, style: "mapbox://styles/mapbox/standard", interactive: false, fadeDuration: 0, });
_map.on("style.load", () => { // Hide all features from the Mapbox Standard style const hideFeatures = [ "showRoadsAndTransit", "showRoads", "showTransit", "showPedestrianRoads", "showRoadLabels", "showTransitLabels", "showPlaceLabels", "showPointOfInterestLabels", "showPointsOfInterest", "showAdminBoundaries", "showLandmarkIcons", "showLandmarkIconLabels", "show3dObjects", "show3dBuildings", "show3dTrees", "show3dLandmarks", "show3dFacades", ]; for (const feature of hideFeatures) { _map.setConfigProperty("basemap", feature, false); }
_map.setConfigProperty("basemap", "colorTrunks", "rgba(0, 0, 0, 0)");
_map.addSource("trace", { type: "geojson", data: { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: lineCoordinates, }, }, }); _map.addLayer({ type: "line", source: "trace", id: "line", paint: { "line-color": "black", "line-width": 5, }, layout: { "line-cap": "round", "line-join": "round", }, }); });
_map.on("load", () => { continueRender(handle); setMap(_map); }); }, [handle, lineCoordinates]);
const style: React.CSSProperties = useMemo( () => ({ width, height, position: "absolute" }), [width, height], );
return <AbsoluteFill ref={ref} style={style} />; };
The following is important in Remotion:
- Animations must be driven by `useCurrentFrame()` and animations that Mapbox brings itself should be disabled. For example, the `fadeDuration` prop should be set to `0`, `interactive` should be set to `false`, etc.
- Loading the map should be delayed using `useDelayRender()` and the map should be set to `null` until it is loaded.
- The element containing the ref MUST have an explicit width and height and `position: "absolute"`.
- Do not add a `_map.remove();` cleanup function.
## Drawing lines
Unless I request it, do not add a glow effect to the lines.
Unless I request it, do not add additional points to the lines.
## Map style
By default, use the `mapbox://styles/mapbox/standard` style.
Hide the labels from the base map style.
Unless I request otherwise, remove all features from the Mapbox Standard style.
// Hide all features from the Mapbox Standard style const hideFeatures = [ "showRoadsAndTransit", "showRoads", "showTransit", "showPedestrianRoads", "showRoadLabels", "showTransitLabels", "showPlaceLabels", "showPointOfInterestLabels", "showPointsOfInterest", "showAdminBoundaries", "showLandmarkIcons", "showLandmarkIconLabels", "show3dObjects", "show3dBuildings", "show3dTrees", "show3dLandmarks", "show3dFacades", ]; for (const feature of hideFeatures) { _map.setConfigProperty("basemap", feature, false); }
_map.setConfigProperty("basemap", "colorMotorways", "transparent"); _map.setConfigProperty("basemap", "colorRoads", "transparent"); _map.setConfigProperty("basemap", "colorTrunks", "transparent");
## Animating the camera
You can animate the camera along the line by adding a `useEffect` hook that updates the camera position based on the current frame.
Unless I ask for it, do not jump between camera angles.
import * as turf from "@turf/turf"; import { interpolate } from "remotion"; import { Easing } from "remotion"; import { useCurrentFrame, useVideoConfig, useDelayRender } from "remotion";
const animationDuration = 20; const cameraAltitude = 4000;
const frame = useCurrentFrame(); const { fps } = useVideoConfig(); const { delayRender, continueRender } = useDelayRender();
useEffect(() => { if (!map) { return; } const handle = delayRender("Moving point...");
const routeDistance = turf.length(turf.lineString(lineCoordinates));
const progress = interpolate( frame / fps, [0.00001, animationDuration], [0, 1], { easing: Easing.inOut(Easing.sin), extrapolateLeft: "clamp", extrapolateRight: "clamp", }, );
const camera = map.getFreeCameraOptions();
const alongRoute = turf.along( turf.lineString(lineCoordinates), routeDistance * progress, ).geometry.coordinates;
camera.lookAtPoint({ lng: alongRoute[0], lat: alongRoute[1], });
map.setFreeCameraOptions(camera); map.once("idle", () => continueRender(handle)); }, [lineCoordinates, fps, frame, handle, map]);
Notes:
IMPORTANT: Keep the camera by default so north is up.
IMPORTANT: For multi-step animations, set all properties at all stages (zoom, position, line progress) to prevent jumps. Override initial values.
- The progress is clamped to a minimum value to avoid the line being empty, which can lead to turf errors
- See [Timing](./timing.md) for more options for timing.
- Consider the dimensions of the composition and make the lines thick enough and the label font size large enough to be legible for when the composition is scaled down.
## Animating lines
### Straight lines (linear interpolation)
To animate a line that appears straight on the map, use linear interpolation between coordinates. Do NOT use turf's `lineSliceAlong` or `along` functions, as they use geodesic (great circle) calculations which appear curved on a Mercator projection.
const frame = useCurrentFrame(); const { durationInFrames } = useVideoConfig();
useEffect(() => { if (!map) return;
const animationHandle = delayRender("Animating line...");
const progress = interpolate(frame, [0, durationInFrames - 1], [0, 1], { extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.inOut(Easing.cubic), });
// Linear interpolation for a straight line on the map const start = lineCoordinates[0]; const end = lineCoordinates[1]; const currentLng = start[0] + (end[0] - start[0]) progress; const currentLat = start[1] + (end[1] - start[1]) progress;
const lineData: GeoJSON.Feature<GeoJSON.LineString> = { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates: [start, [currentLng, currentLat]], }, };
const source = map.getSource("trace") as mapboxgl.GeoJSONSource; if (source) { source.setData(lineData); }
map.once("idle", () => continueRender(animationHandle)); }, [frame, map, durationInFrames]);
### Curved lines (geodesic/great circle)
To animate a line that follows the geodesic (great circle) path between two points, use turf's `lineSliceAlong`. This is useful for showing flight paths or the actual shortest distance on Earth.
import * as turf from "@turf/turf";
const routeLine = turf.lineString(lineCoordinates); const routeDistance = turf.length(routeLine);
const currentDistance = Math.max(0.001, routeDistance * progress); const slicedLine = turf.lineSliceAlong(routeLine, 0, currentDistance);
const source = map.getSource("route") as mapboxgl.GeoJSONSource; if (source) { source.setData(slicedLine); }
## Markers
Add labels, and markers where appropriate.
_map.addSource("markers", { type: "geojson", data: { type: "FeatureCollection", features: [ { type: "Feature", properties: { name: "Point 1" }, geometry: { type: "Point", coordinates: [-118.2437, 34.0522] }, }, ], }, });
_map.addLayer({ id: "city-markers", type: "circle", source: "markers", paint: { "circle-radius": 40, "circle-color": "#FF4444", "circle-stroke-width": 4, "circle-stroke-color": "#FFFFFF", }, });
_map.addLayer({ id: "labels", type: "symbol", source: "markers", layout: { "text-field": ["get", "name"], "text-font": ["DIN Pro Bold", "Arial Unicode MS Bold"], "text-size": 50, "text-offset": [0, 0.5], "text-anchor": "top", }, paint: { "text-color": "#FFFFFF", "text-halo-color": "#000000", "text-halo-width": 2, }, });
Make sure they are big enough. Check the composition dimensions and scale the labels accordingly.
For a composition size of 1920x1080, the label font size should be at least 40px.
IMPORTANT: Keep the `text-offset` small enough so it is close to the marker. Consider the marker circle radius. For a circle radius of 40, this is a good offset:
"text-offset": [0, 0.5],
## 3D buildings
To enable 3D buildings, use the following code:
_map.setConfigProperty("basemap", "show3dObjects", true); _map.setConfigProperty("basemap", "show3dLandmarks", true); _map.setConfigProperty("basemap", "show3dBuildings", true);
## Rendering
When rendering a map animation, make sure to render with the following flags:
npx remotion render --gl=angle --concurrency=1
Measuring DOM nodes in Remotion
Remotion applies a scale() transform to the video container, which affects values from getBoundingClientRect(). Use useCurrentScale() to get correct measurements.
Measuring element dimensions
import { useCurrentScale } from "remotion";
import { useRef, useEffect, useState } from "react";
export const MyComponent = () => {
const ref = useRef<HTMLDivElement>(null);
const scale = useCurrentScale();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
setDimensions({
width: rect.width / scale,
height: rect.height / scale,
});
}, [scale]);
return <div ref={ref}>Content to measure</div>;
};Measuring text in Remotion
Prerequisites
Install @remotion/layout-utils if it is not already installed:
npx remotion add @remotion/layout-utilsMeasuring text dimensions
Use measureText() to calculate the width and height of text:
import { measureText } from "@remotion/layout-utils";
const { width, height } = measureText({
text: "Hello World",
fontFamily: "Arial",
fontSize: 32,
fontWeight: "bold",
});Results are cached - duplicate calls return the cached result.
Fitting text to a width
Use fitText() to find the optimal font size for a container:
import { fitText } from "@remotion/layout-utils";
const { fontSize } = fitText({
text: "Hello World",
withinWidth: 600,
fontFamily: "Inter",
fontWeight: "bold",
});
return (
<div
style={{
fontSize: Math.min(fontSize, 80), // Cap at 80px
fontFamily: "Inter",
fontWeight: "bold",
}}
>
Hello World
</div>
);Checking text overflow
Use fillTextBox() to check if text exceeds a box:
import { fillTextBox } from "@remotion/layout-utils";
const box = fillTextBox({ maxBoxWidth: 400, maxLines: 3 });
const words = ["Hello", "World", "This", "is", "a", "test"];
for (const word of words) {
const { exceedsBox } = box.add({
text: word + " ",
fontFamily: "Arial",
fontSize: 24,
});
if (exceedsBox) {
// Text would overflow, handle accordingly
break;
}
}Best practices
Load fonts first: Only call measurement functions after fonts are loaded.
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily, waitUntilDone } = loadFont("normal", {
weights: ["400"],
subsets: ["latin"],
});
waitUntilDone().then(() => {
// Now safe to measure
const { width } = measureText({
text: "Hello",
fontFamily,
fontSize: 32,
});
});Use validateFontIsLoaded: Catch font loading issues early:
measureText({
text: "Hello",
fontFamily: "MyCustomFont",
fontSize: 32,
validateFontIsLoaded: true, // Throws if font not loaded
});Match font properties: Use the same properties for measurement and rendering:
const fontStyle = {
fontFamily: "Inter",
fontSize: 32,
fontWeight: "bold" as const,
letterSpacing: "0.5px",
};
const { width } = measureText({
text: "Hello",
...fontStyle,
});
return <div style={fontStyle}>Hello</div>;Avoid padding and border: Use outline instead of border to prevent layout differences:
<div style={{ outline: "2px solid red" }}>Text</div>To make a video parametrizable, a Zod schema can be added to a composition.
First, zod must be installed .
Search the project for lockfiles and run the correct command depending on the package manager:
If package-lock.json is found, use the following command:
npm i zodIf bun.lockb is found, use the following command:
bun i zodIf yarn.lock is found, use the following command:
yarn add zodIf pnpm-lock.yaml is found, use the following command:
pnpm i zodThen, a Zod schema can be defined alongside the component:
```tsx title="src/MyComposition.tsx" import { z } from "zod";
export const MyCompositionSchema = z.object({ title: z.string(), });
const MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = () => { return ( <div> <h1>{props.title}</h1> </div> ); };
In the root file, the schema can be passed to the composition:
import { Composition } from "remotion"; import { MycComponent, MyCompositionSchema } from "./MyComposition";
export const RemotionRoot = () => { return ( <Composition id="MyComposition" component={MyComponent} durationInFrames={100} fps={30} width={1080} height={1080} defaultProps={{ title: "Hello World" }} schema={MyCompositionSchema} /> ); };
Now, the user can edit the parameter visually in the sidebar.
All schemas that are supported by Zod are supported by Remotion.
Remotion requires that the top-level type is a z.object(), because the collection of props of a React component is always an object.
## Color picker
For adding a color picker, use `zColor()` from `@remotion/zod-types`.
If it is not installed, use the following command:
npx remotion add @remotion/zod-types # If project uses npm bunx remotion add @remotion/zod-types # If project uses bun yarn remotion add @remotion/zod-types # If project uses yarn pnpm exec remotion add @remotion/zod-types # If project uses pnpm
Then import `zColor` from `@remotion/zod-types`:
import { zColor } from "@remotion/zod-types";
Then use it in the schema:
export const MyCompositionSchema = z.object({ color: zColor(), });
Use <Sequence> to delay when an element appears in the timeline.
import { Sequence } from "remotion";
export const Title = () => {
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 }}>Title</div>;
};
export const Subtitle = () => {
return <div>Subtitle</div>;
};
const Main = () => {
const {fps} = useVideoConfig();
return (
<AbsoluteFill>
<Sequence>
<Background />
</Sequence>
<Sequence from={1 * fps} durationInFrames={2 * fps} layout="none">
<Title />
</Sequence>
<Sequence from={2 * fps} durationInFrames={2 * fps} layout="none">
<Subtitle />
</Sequence>
</AbsoluteFill>
);
}This will by default wrap the component in an absolute fill element. If the items should not be wrapped, use the layout prop:
<Sequence layout="none">
<Title />
</Sequence>Premounting
This loads the component in the timeline before it is actually played. Always premount any <Sequence>!
<Sequence premountFor={1 * fps}>
<Title />
</Sequence>Series
Use <Series> when elements should play one after another 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>;Same as with <Sequence>, the items will be wrapped in an absolute fill element by default when using <Series.Sequence>, unless the layout prop is set to none.
Series with overlaps
Use negative offset for overlapping sequences:
<Series>
<Series.Sequence durationInFrames={60}>
<SceneA />
</Series.Sequence>
<Series.Sequence offset={-15} durationInFrames={60}>
{/* Starts 15 frames before SceneA ends */}
<SceneB />
</Series.Sequence>
</Series>Frame References Inside Sequences
Inside a Sequence, useCurrentFrame() returns the local frame (starting from 0):
<Sequence from={60} durationInFrames={30}>
<MyComponent />
{/* Inside MyComponent, useCurrentFrame() returns 0-29, not 60-89 */}
</Sequence>Nested Sequences
Sequences can be nested for complex timing:
<Sequence from={0} durationInFrames={120}>
<Background />
<Sequence from={15} durationInFrames={90} layout="none">
<Title />
</Sequence>
<Sequence from={45} durationInFrames={60} layout="none">
<Subtitle />
</Sequence>
</Sequence>Nesting compositions within another
To add a composition within another composition, you can use the <Sequence> component with a width and height prop to specify the size of the composition.
<AbsoluteFill>
<Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>
<CompositionComponent />
</Sequence>
</AbsoluteFill>To include a sound effect, use the <Audio> tag:
import { Audio } from "@remotion/sfx";
<Audio src={"https://remotion.media/whoosh.wav"} />;The following sound effects are available:
https://remotion.media/whoosh.wavhttps://remotion.media/whip.wavhttps://remotion.media/page-turn.wavhttps://remotion.media/switch.wavhttps://remotion.media/mouse-click.wavhttps://remotion.media/shutter-modern.wavhttps://remotion.media/shutter-old.wavhttps://remotion.media/ding.wavhttps://remotion.media/bruh.wavhttps://remotion.media/vine-boom.wavhttps://remotion.media/windows-xp-error.wav
For more sound effects, search the internet. A good resource is https://github.com/kapishdima/soundcn/tree/main/assets.
Adaptive Silence Detection
Detect silent segments in video or audio files.
Requires FFmpeg — see ffmpeg.md for how to invoke it in Remotion projects.
Step 1: Measure loudness with loudnorm
Use the loudnorm filter in JSON mode to get the EBU R128 integrated loudness and gating threshold for each file:
npx remotion ffmpeg -i public/video.mov -map 0:a -af loudnorm=print_format=json -f null /dev/nullAs output you will get:
input_i: Integrated loudness (dB) — the overall perceived volumeinput_thresh: EBU R128 gating threshold (dB) — the level below which audio is considered too quiet to count toward loudness measurement
Step 2: Detect silences using adaptive threshold
Pass the input_thresh value from step 1 as the noise parameter to silencedetect:
npx remotion ffmpeg -i public/video.mov -map 0:a -af "silencedetect=noise=${THRESH}dB:d=0.5" -f null /dev/nullParameters:
noise: The threshold below which audio is considered silent. Useinput_threshfrom step 1.d: Minimum silence duration in seconds.0.5is a good default.
Interpreting the output
The filter outputs pairs of silence_start and silence_end timestamps:
[silencedetect] silence_start: 0
[silencedetect] silence_end: 2.241021 | silence_duration: 2.241021
[silencedetect] silence_start: 38.77425
[silencedetect] silence_end: 39.619604 | silence_duration: 0.845354Identifying leading and trailing silence
- Leading silence: Consecutive silence segments starting at or near 0. If the first
silence_startis > 0.5s, there is no leading silence. - Trailing silence: The last silence segment that extends to (or near) the end of the file. Compare the last
silence_endwith the file's total duration.
When multiple silences are nearly contiguous at the start or end (gap < 0.2s), treat them as a single leading/trailing silence block.
Using with Remotion's <Video> component
Apply the detected trim points using trimBefore and trimAfter (values are in frames):
import { Video } from "@remotion/media";
import { staticFile, useVideoConfig } from "remotion";
const { fps } = useVideoConfig();
<Video
src={staticFile("video.mov")}
trimBefore={Math.floor(leadingEnd * fps)}
trimAfter={Math.ceil(trailingStart * fps)}
/>All captions must be processed in JSON. The captions must use the Caption type which is the following:
import type { Caption } from "@remotion/captions";This is the definition:
type Caption = {
text: string;
startMs: number;
endMs: number;
timestampMs: number | null;
confidence: number | null;
};Generating captions
To transcribe video and audio files to generate captions, load the ./transcribe-captions.md file for more instructions.
Displaying captions
To display captions in your video, load the ./display-captions.md file for more instructions.
Importing captions
To import captions from a .srt file, load the ./import-srt-captions.md file for more instructions.
You can and should use TailwindCSS in Remotion, if TailwindCSS is installed in the project.
Don't use transition-* or animate-* classes - always animate using the useCurrentFrame() hook.
Tailwind must be installed and enabled first in a Remotion project - fetch https://www.remotion.dev/docs/tailwind using WebFetch for instructions.
Text animations
Based on useCurrentFrame(), reduce the string character by character to create a typewriter effect.
Typewriter Effect
See Typewriter for an advanced example with a blinking cursor and a pause after the first sentence.
Always use string slicing for typewriter effects. Never use per-character opacity.
Word Highlighting
See Word Highlight for an example for how a word highlight is animated, like with a highlighter pen.
Drive motion with interpolate() over explicit frame range. To customize timing, use `Easing.bezier`. The four parameters are the same as CSS cubic-bezier(x1, y1, x2, y2).
A simple linear interpolation is done using the interpolate function.
```ts title="Going from 0 to 1 over 100 frames" import { interpolate } from "remotion";
const opacity = interpolate(frame, [0, 100], [0, 1]);
By default, the values are not clamped, so the value can go outside the range [0, 1].
Here is how they can be clamped:
const opacity = interpolate(frame, [0, 100], [0, 1], { extrapolateRight: "clamp", extrapolateLeft: "clamp", });
## Bézier easing
Use `Easing.bezier(x1, y1, x2, y2)` inside the `interpolate` options object. The curve is identical in spirit to CSS animations and transitions, which helps when you are stealing timing from the web or from a designer’s spec.
import { interpolate, Easing } from "remotion";
const opacity = interpolate(frame, [0, 60], [0, 1], { easing: Easing.bezier(0.16, 1, 0.3, 1), extrapolateLeft: "clamp", extrapolateRight: "clamp", });
### Examples (copy-paste curves)
**1. Crisp UI entrance (strong ease-out, no overshoot)** — slows nicely into the rest value; similar to many system “deceleration” curves.
const enter = interpolate(frame, [0, 45], [0, 1], { easing: Easing.bezier(0.16, 1, 0.3, 1), extrapolateLeft: "clamp", extrapolateRight: "clamp", });
**2. Editorial / slow fade (balanced ease-in-out)** — symmetric acceleration and deceleration over a hold-friendly move.
const progress = interpolate(frame, [0, 90], [0, 1], { easing: Easing.bezier(0.45, 0, 0.55, 1), extrapolateLeft: "clamp", extrapolateRight: "clamp", });
**3. Playful overshoot (control point y > 1)** — a little past the target then settles; use sparingly for emphasis.
const pop = interpolate(frame, [0, 30], [0, 1], { easing: Easing.bezier(0.34, 1.56, 0.64, 1), extrapolateLeft: "clamp", extrapolateRight: "clamp", });
## Preset easings (`Easing.in` / `Easing.out` / named curves)
Easing can be added to the `interpolate` function without a custom cubic:
import { interpolate, Easing } from "remotion";
const value1 = interpolate(frame, [0, 100], [0, 1], { easing: Easing.inOut(Easing.cubic), extrapolateLeft: "clamp", extrapolateRight: "clamp", });
The default easing is `Easing.linear`.
Convexities:
- `Easing.in` — starting slow and accelerating
- `Easing.out` — starting fast and slowing down
- `Easing.inOut`
Named curves (from most linear to most curved):
- `Easing.quad`
- `Easing.cubic` (good default when you do not need a custom cubic)
- `Easing.sin`
- `Easing.exp`
- `Easing.circle`
### Easing direction for enter/exit animations
Use `Easing.out` for enter animations (starts fast, decelerates into place) and `Easing.in` for exit animations (starts slow, accelerates away). This feels natural because elements arrive with momentum and leave with gravity. When you need a specific curve from design, prefer a single `Easing.bezier(...)` instead of stacking presets.
## Composing interpolations
When multiple properties share the same timing (e.g. a slide-in panel and a video shift), avoid duplicating the full interpolation for each property. Instead, create a single normalized progress value (0 to 1) and derive each property from it:
const slideIn = interpolate( frame, [slideInStart, slideInStart + slideInDuration], [0, 1], { easing: Easing.bezier(0.22, 1, 0.36, 1), extrapolateLeft: "clamp", extrapolateRight: "clamp", }, ); const slideOut = interpolate( frame, [slideOutStart, slideOutStart + slideOutDuration], [0, 1], { easing: Easing.in(Easing.cubic), extrapolateLeft: "clamp", extrapolateRight: "clamp" }, ); const progress = slideIn - slideOut;
// Derive multiple properties from the same progress const overlayX = interpolate(progress, [0, 1], [100, 0]); const videoX = interpolate(progress, [0, 1], [0, -20]); const opacity = interpolate(progress, [0, 1], [0, 1]);
The key idea: separate **timing** (when and how fast) from **mapping** (what values to animate between).
Transcribing audio
To transcribe audio to generate captions in Remotion, you can use the `transcribe()` function from the `@remotion/install-whisper-cpp` package.
Prerequisites
First, the @remotion/install-whisper-cpp package needs to be installed. If it is not installed, use the following command:
npx remotion add @remotion/install-whisper-cppTranscribing
Make a Node.js script to download Whisper.cpp and a model, and transcribe the audio.
import path from "path";
import {
downloadWhisperModel,
installWhisperCpp,
transcribe,
toCaptions,
} from "@remotion/install-whisper-cpp";
import fs from "fs";
const to = path.join(process.cwd(), "whisper.cpp");
await installWhisperCpp({
to,
version: "1.5.5",
});
await downloadWhisperModel({
model: "medium.en",
folder: to,
});
// Convert the audio to a 16KHz wav file first if needed:
// import {execSync} from 'child_process';
// execSync('ffmpeg -i /path/to/audio.mp4 -ar 16000 /path/to/audio.wav -y');
const whisperCppOutput = await transcribe({
model: "medium.en",
whisperPath: to,
whisperCppVersion: "1.5.5",
inputPath: "/path/to/audio123.wav",
tokenLevelTimestamps: true,
});
// Optional: Apply our recommended postprocessing
const { captions } = toCaptions({
whisperCppOutput,
});
// Write it to the public/ folder so it can be fetched from Remotion
fs.writeFileSync("captions123.json", JSON.stringify(captions, null, 2));Transcribe each clip individually and create multiple JSON files.
See Displaying captions for how to display the captions in Remotion.
TransitionSeries
<TransitionSeries> arranges scenes and supports two ways to enhance the cut point between them:
- Transitions (
<TransitionSeries.Transition>) — crossfade, slide, wipe, etc. between two scenes. Shortens the timeline because both scenes play simultaneously during the transition. - Overlays (
<TransitionSeries.Overlay>) — render an effect (e.g. a light leak) on top of the cut point without shortening the timeline.
Children are absolutely positioned.
Prerequisites
npx remotion add @remotion/transitionsTransition example
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
</TransitionSeries>;Overlay example
Any React component can be used as an overlay. For a ready-made effect, see the light-leaks rule.
import { TransitionSeries } from "@remotion/transitions";
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>;Mixing transitions and overlays
Transitions and overlays can coexist in the same <TransitionSeries>, but an overlay cannot be adjacent to a transition or another overlay.
import { TransitionSeries, linearTiming } from "@remotion/transitions";
import { fade } from "@remotion/transitions/fade";
import { LightLeak } from "@remotion/light-leaks";
<TransitionSeries>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneA />
</TransitionSeries.Sequence>
<TransitionSeries.Overlay durationInFrames={30}>
<LightLeak />
</TransitionSeries.Overlay>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneB />
</TransitionSeries.Sequence>
<TransitionSeries.Transition
presentation={fade()}
timing={linearTiming({ durationInFrames: 15 })}
/>
<TransitionSeries.Sequence durationInFrames={60}>
<SceneC />
</TransitionSeries.Sequence>
</TransitionSeries>;Transition props
<TransitionSeries.Transition> requires:
presentation— the visual effect (e.g.fade(),slide(),wipe()).timing— controls speed and easing (e.g.linearTiming(),springTiming()).
Overlay props
<TransitionSeries.Overlay> accepts:
durationInFrames— how long the overlay is visible (positive integer).offset?— shifts the overlay relative to the cut point center. Positive = later, negative = earlier. Default:0.
Available transition types
Import transitions from their respective modules:
import { fade } from "@remotion/transitions/fade";
import { slide } from "@remotion/transitions/slide";
import { wipe } from "@remotion/transitions/wipe";
import { flip } from "@remotion/transitions/flip";
import { clockWipe } from "@remotion/transitions/clock-wipe";Slide transition with direction
import { slide } from "@remotion/transitions/slide";
<TransitionSeries.Transition
presentation={slide({ direction: "from-left" })}
timing={linearTiming({ durationInFrames: 20 })}
/>;Directions: "from-left", "from-right", "from-top", "from-bottom"
Timing options
import { linearTiming, springTiming } from "@remotion/transitions";
// Linear timing - constant speed
linearTiming({ durationInFrames: 20 });
// Spring timing - organic motion
springTiming({ config: { damping: 200 }, durationInFrames: 25 });Duration calculation
Transitions overlap adjacent scenes, so the total composition length is shorter than the sum of all sequence durations. Overlays do not affect the total duration.
For example, with two 60-frame sequences and a 15-frame transition:
- Without transitions:
60 + 60 = 120frames - With transition:
60 + 60 - 15 = 105frames
Adding an overlay between two other sequences does not change the total.
Getting the duration of a transition
Use the getDurationInFrames() method on the timing object:
import { linearTiming, springTiming } from "@remotion/transitions";
const linearDuration = linearTiming({
durationInFrames: 20,
}).getDurationInFrames({ fps: 30 });
// Returns 20
const springDuration = springTiming({
config: { damping: 200 },
}).getDurationInFrames({ fps: 30 });
// Returns calculated duration based on spring physicsFor springTiming without an explicit durationInFrames, the duration depends on fps because it calculates when the spring animation settles.
Calculating total composition duration
import { linearTiming } from "@remotion/transitions";
const scene1Duration = 60;
const scene2Duration = 60;
const scene3Duration = 60;
const timing1 = linearTiming({ durationInFrames: 15 });
const timing2 = linearTiming({ durationInFrames: 20 });
const transition1Duration = timing1.getDurationInFrames({ fps: 30 });
const transition2Duration = timing2.getDurationInFrames({ fps: 30 });
const totalDuration =
scene1Duration +
scene2Duration +
scene3Duration -
transition1Duration -
transition2Duration;
// 60 + 60 + 60 - 15 - 20 = 145 framesRendering Transparent Videos
Remotion can render transparent videos in two ways: as a ProRes video or as a WebM video.
Transparent ProRes
Ideal for when importing into video editing software.
CLI:
npx remotion render --image-format=png --pixel-format=yuva444p10le --codec=prores --prores-profile=4444 MyComp out.movDefault in Studio (restart Studio after changing):
// remotion.config.ts
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("png");
Config.setPixelFormat("yuva444p10le");
Config.setCodec("prores");
Config.setProResProfile("4444");Setting it as the default export settings for a composition (using calculateMetadata):
import { CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
return {
defaultCodec: "prores",
defaultVideoImageFormat: "png",
defaultPixelFormat: "yuva444p10le",
defaultProResProfile: "4444",
};
};
<Composition
id="my-video"
component={MyVideo}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
calculateMetadata={calculateMetadata}
/>;Transparent WebM (VP9)
Ideal for when playing in a browser.
CLI:
npx remotion render --image-format=png --pixel-format=yuva420p --codec=vp9 MyComp out.webmDefault in Studio (restart Studio after changing):
// remotion.config.ts
import { Config } from "@remotion/cli/config";
Config.setVideoImageFormat("png");
Config.setPixelFormat("yuva420p");
Config.setCodec("vp9");Setting it as the default export settings for a composition (using calculateMetadata):
import { CalculateMetadataFunction } from "remotion";
const calculateMetadata: CalculateMetadataFunction<Props> = async ({
props,
}) => {
return {
defaultCodec: "vp8",
defaultVideoImageFormat: "png",
defaultPixelFormat: "yuva420p",
};
};
<Composition
id="my-video"
component={MyVideo}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
calculateMetadata={calculateMetadata}
/>;Use <Sequence> with a negative from value to trim the start of an animation.
Trim the Beginning
A negative from value shifts time backwards, making the animation start partway through:
import { Sequence, useVideoConfig } from "remotion";
const fps = useVideoConfig();
<Sequence from={-0.5 * fps}>
<MyAnimation />
</Sequence>;The animation appears 15 frames into its progress - the first 15 frames are trimmed off. Inside <MyAnimation>, useCurrentFrame() starts at 15 instead of 0.
Trim the End
Use durationInFrames to unmount content after a specified duration:
<Sequence durationInFrames={1.5 * fps}>
<MyAnimation />
</Sequence>The animation plays for 45 frames, then the component unmounts.
Trim and Delay
Nest sequences to both trim the beginning and delay when it appears:
<Sequence from={30}>
<Sequence from={-15}>
<MyAnimation />
</Sequence>
</Sequence>The inner sequence trims 15 frames from the start, and the outer sequence delays the result by 30 frames.
Using videos in Remotion
Prerequisites
First, the @remotion/media package needs to be installed. If it is not, use the following command:
npx remotion add @remotion/media # If project uses npm
bunx remotion add @remotion/media # If project uses bun
yarn remotion add @remotion/media # If project uses yarn
pnpm exec remotion add @remotion/media # If project uses pnpmUse <Video> from @remotion/media to embed videos into your composition.
import { Video } from "@remotion/media";
import { staticFile } from "remotion";
export const MyComposition = () => {
return <Video src={staticFile("video.mp4")} />;
};Remote URLs are also supported:
<Video src="https://remotion.media/video.mp4" />Trimming
Use trimBefore and trimAfter to remove portions of the video. Values are in seconds.
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
trimBefore={2 * fps} // Skip the first 2 seconds
trimAfter={10 * fps} // End at the 10 second mark
/>
);Delaying
Wrap the video in a <Sequence> to delay when it appears:
import { Sequence, staticFile } from "remotion";
import { Video } from "@remotion/media";
const { fps } = useVideoConfig();
return (
<Sequence from={1 * fps}>
<Video src={staticFile("video.mp4")} />
</Sequence>
);The video will appear after 1 second.
Sizing and Position
Use the style prop to control size and position:
<Video
src={staticFile("video.mp4")}
style={{
width: 500,
height: 300,
position: "absolute",
top: 100,
left: 50,
objectFit: "cover",
}}
/>Volume
Set a static volume (0 to 1):
<Video src={staticFile("video.mp4")} volume={0.5} />Or use a callback for dynamic volume based on the current frame:
import { interpolate } from "remotion";
const { fps } = useVideoConfig();
return (
<Video
src={staticFile("video.mp4")}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: "clamp" })
}
/>
);Use muted to silence the video entirely:
<Video src={staticFile("video.mp4")} muted />Speed
Use playbackRate to change the playback speed:
<Video src={staticFile("video.mp4")} playbackRate={2} /> {/* 2x speed */}
<Video src={staticFile("video.mp4")} playbackRate={0.5} /> {/* Half speed */}Reverse playback is not supported.
Looping
Use loop to loop the video indefinitely:
<Video src={staticFile("video.mp4")} loop />Use loopVolumeCurveBehavior to control how the frame count behaves when looping:
"repeat": Frame count resets to 0 each loop (forvolumecallback)"extend": Frame count continues incrementing
<Video
src={staticFile("video.mp4")}
loop
loopVolumeCurveBehavior="extend"
volume={(f) => interpolate(f, [0, 300], [1, 0])} // Fade out over multiple loops
/>Pitch
Use toneFrequency to adjust the pitch without affecting speed. Values range from 0.01 to 2:
<Video
src={staticFile("video.mp4")}
toneFrequency={1.5} // Higher pitch
/>
<Video
src={staticFile("video.mp4")}
toneFrequency={0.8} // Lower pitch
/>Pitch shifting only works during server-side rendering, not in the Remotion Studio preview or in the <Player />.
Adding AI voiceover to a Remotion composition
Use ElevenLabs TTS to generate speech audio per scene, then use `calculateMetadata` to dynamically size the composition to match the audio.
Prerequisites
By default this guide uses ElevenLabs as the TTS provider (ELEVENLABS_API_KEY environment variable). Users may substitute any TTS service that can produce an audio file.
If the user has not specified a TTS provider, recommend ElevenLabs and ask for their API key.
Ensure the environment variable is available when running the generation script:
node --strip-types generate-voiceover.tsGenerating audio with ElevenLabs
Create a script that reads the config, calls the ElevenLabs API for each scene, and writes MP3 files to the public/ directory so Remotion can access them via staticFile().
The core API call for a single scene:
``ts title="generate-voiceover.ts" 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/${compositionId}/${scene.id}.mp3, audioBuffer);
## Dynamic composition duration with calculateMetadata
Use [`calculateMetadata`](./calculate-metadata.md) to measure the [audio durations](./get-audio-duration.md) and set the composition length accordingly.
import { CalculateMetadataFunction, staticFile } from "remotion"; import { getAudioDuration } from "./get-audio-duration";
const FPS = 30;
const SCENE_AUDIO_FILES = [ "voiceover/my-comp/scene-01-intro.mp3", "voiceover/my-comp/scene-02-main.mp3", "voiceover/my-comp/scene-03-outro.mp3", ];
export const calculateMetadata: CalculateMetadataFunction<Props> = async ({ props, }) => { const durations = await Promise.all( SCENE_AUDIO_FILES.map((file) => getAudioDuration(staticFile(file))), );
const sceneDurations = durations.map((durationInSeconds) => { return durationInSeconds * FPS; });
return { durationInFrames: Math.ceil(sceneDurations.reduce((sum, d) => sum + d, 0)), }; };
The computed `sceneDurations` are passed into the component via a `voiceover` prop so the component knows how long each scene should be.
If the composition uses [`<TransitionSeries>`](./transitions.md), subtract the overlap from total duration: [./transitions.md#calculating-total-composition-duration](./transitions.md#calculating-total-composition-duration)
## Rendering audio in the component
See [audio.md](./audio.md) for more information on how to render audio in the component.
## Delaying audio start
See [audio.md#delaying](./audio.md#delaying) for more information on how to delay the audio start.
Related skills
FAQ
Is Remotion Best Practices safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.