
Remotion
- 82 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
remotion is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- remotion
- AI & Agent Building
- AI-coding skill
Remotion by the numbers
- 82 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,183 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill remotionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 82 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Remotion
Overview
Remotion enables programmatic video creation using React components. Compositions define renderable videos with explicit width, height, fps, and duration. All animations must be driven by useCurrentFrame() -- CSS animations and Tailwind animation classes are forbidden as they cause rendering artifacts. Use this skill for Remotion compositions, animations, audio, captions, transitions, media handling, or rendering. Not intended for general React UI development.
Quick Reference
| Pattern | API / Approach | Key Points |
|---|---|---|
| Basic animation | useCurrentFrame() + interpolate() | Always clamp with extrapolateRight: 'clamp' |
| Spring animation | spring({ frame, fps }) | { damping: 200 } for smooth, no-bounce motion |
| Composition | <Composition id, component, durationInFrames, fps, width, height> | Always set explicit dimensions |
| Dynamic metadata | calculateMetadata on <Composition> | Set duration, dimensions, props before render |
| Sequencing | <Sequence from, durationInFrames> | useCurrentFrame() returns local frame (starts at 0) |
| Series | <Series> with <Series.Sequence> | Sequential playback; negative offset for overlaps |
| Transitions | <TransitionSeries> with fade(), slide(), wipe() | Total duration = sum of scenes minus transition durations |
| Audio/Video | <Audio> / <Video> from @remotion/media | Use staticFile() for local assets |
| Captions | createTikTokStyleCaptions() | Token-level word highlighting via page.tokens |
| Images | <Img> from remotion | Never use native <img> or Next.js <Image> |
| GIFs | <AnimatedImage> from remotion | Synced with timeline; playbackRate for speed control |
| Fonts | @remotion/google-fonts or @remotion/fonts | Call loadFont() at top level; blocks rendering until ready |
| 3D content | <ThreeCanvas> from @remotion/three | Must set width/height; useFrame() from R3F is forbidden |
| Text measurement | measureText(), fitText() from @remotion/layout-utils | Load fonts first; match properties for measurement and render |
| Parameters | Zod schema on <Composition schema> | Top-level must be z.object(); exact Zod version required (check Remotion docs) |
| Transparent video | --pixel-format=yuva420p --codec=vp9 | WebM for browser; ProRes 4444 for editing software |
| Maps | Mapbox with useCurrentFrame() | Set interactive: false, fadeDuration: 0; render with --gl=angle --concurrency=1 |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using CSS animations or setTimeout | Use interpolate() and useCurrentFrame() for timeline-synced animations |
Using native <img>, <video>, <audio> tags | Use <Img>, <Video>, <Audio> from Remotion for proper preloading |
| Hardcoding video duration | Use calculateMetadata to dynamically set duration from content |
| Not specifying width/height on compositions | Always define explicit dimensions to avoid rendering issues |
Using useFrame() from React Three Fiber | Use useCurrentFrame() from Remotion inside <ThreeCanvas> |
Forgetting premountFor on sequences | Always premount sequences to preload components before playback |
Not clamping interpolate() output | Set extrapolateRight: 'clamp' to prevent values exceeding target range |
Placing <Sequence> in <ThreeCanvas> without layout="none" | Set layout="none" on any <Sequence> inside <ThreeCanvas> |
Delegation
- Discover available Remotion components and their props: Use
Exploreagent to search the codebase for composition definitions and asset usage - Build a multi-scene video with transitions and audio: Use
Taskagent to compose sequences, transitions, and audio tracks step by step - Plan a video generation pipeline with dynamic data: Use
Planagent to design the architecture for parametrized compositions and rendering workflow
References
- Animations and Timing -- Interpolation, springs, easing, and frame-driven animation patterns
- Compositions and Sequencing -- Defining compositions, stills, folders, sequences, series, and trimming
- Media and Assets -- Audio, video, images, GIFs, fonts, and static file handling
- Captions and Text -- Transcription, SRT import, TikTok-style captions, and text animations
- Transitions -- Scene transitions with fade, slide, wipe, flip, and duration calculation
- Advanced Features -- 3D content, charts, maps, parameters, transparent video, and DOM measurement
3D Content (Three.js)
pnpm exec remotion add @remotion/threeWrap 3D content in <ThreeCanvas> with explicit width and height:
import { ThreeCanvas } from '@remotion/three';
import { useVideoConfig, useCurrentFrame } from 'remotion';
const { width, height } = useVideoConfig();
const frame = useCurrentFrame();
const rotationY = frame * 0.02;
<ThreeCanvas width={width} height={height}>
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={0.8} />
<mesh rotation={[0, rotationY, 0]}>
<boxGeometry args={[2, 2, 2]} />
<meshStandardMaterial color="#4a9eff" />
</mesh>
</ThreeCanvas>;useFrame() from @react-three/fiber is forbidden. All animations must use useCurrentFrame(). Shaders and models must not animate on their own.
Any <Sequence> inside <ThreeCanvas> must have layout="none".
Charts
Use regular HTML/SVG or D3.js. Disable all third-party animations -- they cause flickering. Drive all chart animations from useCurrentFrame().
Staggered Bar Chart
const STAGGER_DELAY = 5;
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const bars = data.map((item, i) => {
const progress = spring({
frame: frame - i * STAGGER_DELAY - 10,
fps,
config: { damping: 18, stiffness: 80 },
});
const barHeight = ((item.value - minValue) / range) * chartHeight * progress;
return (
<div key={item.label} style={{ height: barHeight, opacity: progress }} />
);
});Pie Chart with SVG
Animate segments using stroke-dashoffset:
const circumference = 2 * Math.PI * radius;
const segmentLength = (value / total) * circumference;
const progress = interpolate(frame, [0, 100], [0, 1]);
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
<circle
r={radius}
cx={center}
cy={center}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${segmentLength} ${circumference}`}
strokeDashoffset={offset}
transform={`rotate(-90 ${center} ${center})`}
/>;Mapbox Maps
Install mapbox-gl and @turf/turf. Set REMOTION_MAPBOX_TOKEN in .env.
Key Remotion rules for maps:
- Set
interactive: falseandfadeDuration: 0on the Map - Use
useDelayRender()to wait for map load - The map container MUST have explicit
width,height, andposition: "absolute" - Do not add
_map.remove()cleanup - Render with
--gl=angle --concurrency=1
Animate the camera along a line using @turf/turf:
const progress = interpolate(frame / fps, [0, duration], [0, 1], {
easing: Easing.inOut(Easing.sin),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const alongRoute = turf.along(
turf.lineString(lineCoordinates),
routeDistance * progress,
).geometry.coordinates;
const camera = map.getFreeCameraOptions();
camera.lookAtPoint({ lng: alongRoute[0], lat: alongRoute[1] });
map.setFreeCameraOptions(camera);For straight lines on the map, use linear interpolation between coordinates (not turf geodesic functions which appear curved on Mercator projections).
Parameters with Zod
Remotion requires a specific Zod 3.x version (check project dependencies or Remotion docs for the exact version). Remove the ^ prefix from the version number to avoid conflicts. Define a schema alongside the component:
import { z } from 'zod';
export const MyCompositionSchema = z.object({
title: z.string(),
});
const MyComponent: React.FC<z.infer<typeof MyCompositionSchema>> = (props) => {
return <h1>{props.title}</h1>;
};Pass to composition with schema prop. For color pickers, use zColor() from @remotion/zod-types.
Transparent Video
WebM (VP9) -- for browser playback
npx remotion render --image-format=png --pixel-format=yuva420p --codec=vp9 MyComp out.webmProRes 4444 -- for editing software
npx remotion render --image-format=png --pixel-format=yuva444p10le --codec=prores --prores-profile=4444 MyComp out.movSet defaults per composition via calculateMetadata:
const calculateMetadata: CalculateMetadataFunction<Props> = async () => ({
defaultCodec: 'vp8',
defaultVideoImageFormat: 'png',
defaultPixelFormat: 'yuva420p',
});Lottie Animations
pnpm exec remotion add @remotion/lottieFetch the Lottie JSON, use delayRender/continueRender, and render with <Lottie>:
import { Lottie, type LottieAnimationData } from '@remotion/lottie';
<Lottie animationData={animationData} style={{ width: 400, height: 400 }} />;DOM Measurement
Remotion applies a scale() transform. Divide getBoundingClientRect() values by useCurrentScale():
import { useCurrentScale } from 'remotion';
const scale = useCurrentScale();
const rect = ref.current.getBoundingClientRect();
const realWidth = rect.width / scale;
const realHeight = rect.height / scale;Text Measurement
pnpm exec remotion add @remotion/layout-utilsimport { measureText, fitText, fillTextBox } from '@remotion/layout-utils';
const { width, height } = measureText({
text: 'Hello',
fontFamily: 'Arial',
fontSize: 32,
fontWeight: 'bold',
});
const { fontSize } = fitText({
text: 'Hello World',
withinWidth: 600,
fontFamily: 'Inter',
});Load fonts before measuring. Use validateFontIsLoaded: true to catch errors. Match font properties between measurement and rendering.
Use outline instead of border to prevent layout differences in measured text.
All animations in Remotion MUST be driven by useCurrentFrame(). CSS transitions, CSS animations, and Tailwind animation classes are forbidden -- they cause flickering during rendering.
Basic Interpolation
import { useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 2 * fps], [0, 1], {
extrapolateRight: 'clamp',
});By default, values are not clamped and can exceed the target range. Always use extrapolateRight: 'clamp' (and optionally extrapolateLeft: 'clamp') to prevent this.
Write animations in seconds and multiply by fps for frame counts.
Spring Animations
Springs produce natural motion and animate from 0 to 1.
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({ frame, fps });Common Spring Configurations
const smooth = { damping: 200 };
const snappy = { damping: 20, stiffness: 200 };
const bouncy = { damping: 8 };
const heavy = { damping: 15, stiffness: 80, mass: 2 };The default config is mass: 1, damping: 10, stiffness: 100 (slight bounce). Use { damping: 200 } for smooth motion without bounce.
Spring Delay and Duration
const entrance = spring({
frame,
fps,
delay: 20,
durationInFrames: 40,
});Combining Spring with Interpolate
Map spring output (0-1) to custom ranges:
const springProgress = spring({ frame, fps });
const rotation = interpolate(springProgress, [0, 1], [0, 360]);
<div style={{ rotate: rotation + 'deg' }} />;Entrance and Exit
const { fps, durationInFrames } = useVideoConfig();
const inAnimation = spring({ frame, fps });
const outAnimation = spring({
frame,
fps,
durationInFrames: 1 * fps,
delay: durationInFrames - 1 * fps,
});
const scale = inAnimation - outAnimation;Easing Functions
Add easing to interpolate():
import { interpolate, Easing } from 'remotion';
const value = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.inOut(Easing.quad),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});Convexities: Easing.in, Easing.out, Easing.inOut
Curves (most linear to most curved): Easing.quad, Easing.sin, Easing.exp, Easing.circle
Cubic bezier curves:
const value = interpolate(frame, [0, 100], [0, 1], {
easing: Easing.bezier(0.8, 0.22, 0.96, 0.65),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});Staggered Animations
Animate multiple items with incremental delays:
const STAGGER_DELAY = 5;
const bars = data.map((item, i) => {
const delay = i * STAGGER_DELAY;
const height = spring({
frame,
fps,
delay,
config: { damping: 200 },
});
return <div style={{ height: height * item.value }} />;
});Transcription Options
Remotion provides three transcription approaches:
- `@remotion/install-whisper-cpp` -- Local server transcription via Whisper.cpp. Fast and free, requires server infrastructure.
- `@remotion/whisper-web` -- Browser-based transcription via WebAssembly. No server needed, free, slower.
- `@remotion/openai-whisper` -- Cloud transcription via OpenAI Whisper API. Fast, no server, paid.
Importing SRT Subtitles
pnpm exec remotion add @remotion/captionsimport { 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>;
};TikTok-Style Captions
Group captions into pages using createTikTokStyleCaptions():
import { useMemo } from 'react';
import { createTikTokStyleCaptions } from '@remotion/captions';
const SWITCH_CAPTIONS_EVERY_MS = 1200;
const { pages } = useMemo(() => {
return createTikTokStyleCaptions({
captions,
combineTokensWithinMilliseconds: SWITCH_CAPTIONS_EVERY_MS,
});
}, [captions]);Render pages as sequences:
import { Sequence, useVideoConfig, AbsoluteFill } from 'remotion';
const { fps } = useVideoConfig();
<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>;Word Highlighting
Highlight the currently spoken word using page tokens:
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();
const currentTimeMs = (frame / fps) * 1000;
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>
);
};Text Animations
Typewriter Effect
Use string slicing driven by useCurrentFrame(). Never use per-character opacity.
import {
AbsoluteFill,
interpolate,
useCurrentFrame,
useVideoConfig,
} from 'remotion';
const FULL_TEXT = 'From prompt to motion graphics. This is Remotion.';
const CHAR_FRAMES = 2;
export const Typewriter = () => {
const frame = useCurrentFrame();
const typedChars = Math.min(
FULL_TEXT.length,
Math.floor(frame / CHAR_FRAMES),
);
const typedText = FULL_TEXT.slice(0, typedChars);
const cursorOpacity = interpolate(frame % 16, [0, 8, 16], [1, 0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{ backgroundColor: '#fff' }}>
<div style={{ color: '#000', fontSize: 72, fontWeight: 700 }}>
<span>{typedText}</span>
<span style={{ opacity: cursorOpacity }}>{'\u258C'}</span>
</div>
</AbsoluteFill>
);
};Word Highlight Wipe
Animate a highlight background behind a word using spring:
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const highlightProgress = spring({
fps,
frame,
config: { damping: 200 },
delay: 30,
durationInFrames: 18,
});
<span style={{ position: 'relative', display: 'inline-block' }}>
<span
style={{
position: 'absolute',
left: 0,
right: 0,
top: '50%',
height: '1.05em',
transform: `translateY(-50%) scaleX(${highlightProgress})`,
transformOrigin: 'left center',
backgroundColor: '#A7C7E7',
borderRadius: '0.18em',
zIndex: 0,
}}
/>
<span style={{ position: 'relative', zIndex: 1 }}>Remotion</span>
</span>;Compositions
A <Composition> defines the component, width, height, fps, and duration of a renderable video. Place it 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}
/>
);
};Default Props
Pass defaultProps for initial values. Values must be JSON-serializable. Use type declarations (not interface) for prop type safety:
<Composition
id="MyComposition"
component={MyComposition}
durationInFrames={100}
fps={30}
width={1080}
height={1080}
defaultProps={
{ title: 'Hello World', color: '#ff0000' } satisfies MyCompositionProps
}
/>Folders
Organize compositions in the sidebar:
import { Composition, Folder } from 'remotion';
<Folder name="Marketing">
<Composition id="Promo" /* ... */ />
<Composition id="Ad" /* ... */ />
</Folder>;Stills
Use <Still> for single-frame images. No durationInFrames or fps required:
import { Still } from 'remotion';
<Still id="Thumbnail" component={Thumbnail} width={1280} height={720} />;Calculate Metadata
Use calculateMetadata to dynamically set duration, dimensions, and props before rendering:
import { type CalculateMetadataFunction } from 'remotion';
const calculateMetadata: CalculateMetadataFunction<Props> = 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 },
};
};Return fields (all optional): durationInFrames, width, height, fps, props, defaultOutName, defaultCodec.
The abortSignal cancels stale requests when props change in the Studio.
Sequences
Use <Sequence> to delay when an element appears:
import { Sequence, useVideoConfig } from 'remotion';
const { fps } = useVideoConfig();
<Sequence from={1 * fps} durationInFrames={2 * fps} premountFor={1 * fps}>
<Title />
</Sequence>;Always premount sequences with premountFor to preload components.
Inside a <Sequence>, useCurrentFrame() returns the local frame starting at 0, not the composition frame.
By default, sequences wrap children in an absolute fill. Set layout="none" to disable:
<Sequence layout="none">
<Title />
</Sequence>Nesting Compositions
Embed a composition within another using <Sequence> with explicit dimensions:
<AbsoluteFill>
<Sequence width={COMPOSITION_WIDTH} height={COMPOSITION_HEIGHT}>
<CompositionComponent />
</Sequence>
</AbsoluteFill>Series
Use <Series> for sequential playback without overlap:
import { Series } from 'remotion';
<Series>
<Series.Sequence durationInFrames={45}>
<Intro />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<MainContent />
</Series.Sequence>
<Series.Sequence durationInFrames={30}>
<Outro />
</Series.Sequence>
</Series>;Use negative offset for overlapping sequences:
<Series.Sequence offset={-15} durationInFrames={60}>
<SceneB />
</Series.Sequence>Trimming
Trim the Beginning
A negative from shifts time backwards:
<Sequence from={-0.5 * fps}>
<MyAnimation />
</Sequence>Inside MyAnimation, useCurrentFrame() starts at 15 (for 30fps \* 0.5s) instead of 0.
Trim the End
Use durationInFrames to unmount after a set duration:
<Sequence durationInFrames={1.5 * fps}>
<MyAnimation />
</Sequence>Trim and Delay
Nest sequences for both trimming and delaying:
<Sequence from={30}>
<Sequence from={-15}>
<MyAnimation />
</Sequence>
</Sequence>Static Files
Place assets in public/ and reference with staticFile():
import { Img, staticFile } from 'remotion';
<Img src={staticFile('logo.png')} />;Remote URLs work directly without staticFile():
<Img src="https://example.com/image.png" />Audio
Install @remotion/media first:
pnpm exec remotion add @remotion/mediaimport { Audio } from '@remotion/media';
import { staticFile } from 'remotion';
<Audio src={staticFile('audio.mp3')} />;Trimming, Delay, Volume, Speed, Pitch
const { fps } = useVideoConfig();
<Audio
src={staticFile('audio.mp3')}
trimBefore={2 * fps}
trimAfter={10 * fps}
/>
<Sequence from={1 * fps}>
<Audio src={staticFile('audio.mp3')} />
</Sequence>
<Audio src={staticFile('audio.mp3')} volume={0.5} />
<Audio
src={staticFile('audio.mp3')}
volume={(f) =>
interpolate(f, [0, 1 * fps], [0, 1], { extrapolateRight: 'clamp' })
}
/>
<Audio src={staticFile('audio.mp3')} playbackRate={2} />
<Audio src={staticFile('audio.mp3')} loop loopVolumeCurveBehavior="extend" />
<Audio src={staticFile('audio.mp3')} toneFrequency={1.5} />Pitch shifting (toneFrequency) only works during server-side rendering, not in Studio or <Player />.
Video
import { Video } from '@remotion/media';
<Video src={staticFile('video.mp4')} />;Video supports the same props as Audio: trimBefore, trimAfter, volume, muted, playbackRate, loop, toneFrequency. Additionally use style for sizing:
<Video
src={staticFile('video.mp4')}
style={{ width: 500, height: 300, objectFit: 'cover' }}
/>Images
Always use <Img> from remotion. Never use native <img>, Next.js <Image>, or CSS background-image -- <Img> ensures images are loaded before rendering.
import { Img, staticFile } from 'remotion';
<Img src={staticFile('photo.png')} />;Dynamic paths:
<Img src={staticFile(`frames/frame${frame}.png`)} />Get image dimensions:
import { getImageDimensions, staticFile } from 'remotion';
const { width, height } = await getImageDimensions(staticFile('photo.png'));Animated Images (GIFs)
Use <AnimatedImage> for GIF, APNG, AVIF, or WebP synchronized with the timeline:
import { AnimatedImage, staticFile } from 'remotion';
<AnimatedImage src={staticFile('animation.gif')} width={500} height={500} />;Control playback: playbackRate, loopBehavior ("loop", "pause-after-finish", "clear-after-finish"), fit ("fill", "contain", "cover").
Get GIF duration (requires @remotion/gif):
import { getGifDurationInSeconds } from '@remotion/gif';
const duration = await getGifDurationInSeconds(staticFile('animation.gif'));Fonts
Google Fonts
pnpm exec remotion add @remotion/google-fontsimport { loadFont } from '@remotion/google-fonts/Roboto';
const { fontFamily } = loadFont('normal', {
weights: ['400', '700'],
subsets: ['latin'],
});Local Fonts
pnpm exec remotion add @remotion/fontsimport { loadFont } from '@remotion/fonts';
import { staticFile } from 'remotion';
await loadFont({
family: 'MyFont',
url: staticFile('MyFont-Regular.woff2'),
weight: '400',
});Load multiple weights with the same family name and different weight values.
Call loadFont() at the top level of your component or in a separate file imported early.
Mediabunny Utilities
Mediabunny provides browser/Node/Bun utilities for media metadata:
Get Video/Audio Duration
import { Input, ALL_FORMATS, UrlSource } from 'mediabunny';
export const getMediaDuration = async (src: string) => {
const input = new Input({
formats: ALL_FORMATS,
source: new UrlSource(src, { getRetryDelay: () => null }),
});
return input.computeDuration();
};Get Video Dimensions
const videoTrack = await input.getPrimaryVideoTrack();
const width = videoTrack.displayWidth;
const height = videoTrack.displayHeight;Check Decode Compatibility
const videoTrack = await input.getPrimaryVideoTrack();
if (videoTrack && !(await videoTrack.canDecode())) {
return false;
}Prerequisites
pnpm exec remotion add @remotion/transitionsBasic Usage
Use <TransitionSeries> to animate between scenes:
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>;Available Transition Types
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 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';
linearTiming({ durationInFrames: 20 });
springTiming({ config: { damping: 200 }, durationInFrames: 25 });For springTiming without explicit durationInFrames, the duration depends on fps because it calculates when the spring settles.
Duration Calculation
Transitions overlap adjacent scenes, making the total composition shorter:
Two 60-frame scenes + 15-frame transition:
Without transitions: 60 + 60 = 120 frames
With transition: 60 + 60 - 15 = 105 framesGetting Transition Duration
const timing = linearTiming({ durationInFrames: 20 });
const duration = timing.getDurationInFrames({ fps: 30 });Calculating Total Composition Duration
const scene1Duration = 60;
const scene2Duration = 60;
const scene3Duration = 60;
const timing1 = linearTiming({ durationInFrames: 15 });
const timing2 = linearTiming({ durationInFrames: 20 });
const totalDuration =
scene1Duration +
scene2Duration +
scene3Duration -
timing1.getDurationInFrames({ fps: 30 }) -
timing2.getDurationInFrames({ fps: 30 });