remotion-video
Use this skill when creating programmatic videos with Remotion, building React-based video compositions, implementing animations (text, element, scene transitions), rendering videos to MP4/WebM, or setting up standalone Remotion projects. Triggers on video creation, Remotion compositions, useCurrentFrame, interpolate, spring animations, and video rendering.
video remotionvideoanimationreactrenderingprogrammatic-videoWhat is remotion-video?
Use this skill when creating programmatic videos with Remotion, building React-based video compositions, implementing animations (text, element, scene transitions), rendering videos to MP4/WebM, or setting up standalone Remotion projects. Triggers on video creation, Remotion compositions, useCurrentFrame, interpolate, spring animations, and video rendering.
remotion-video
remotion-video is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Creating programmatic videos with Remotion, building React-based video compositions, implementing animations (text, element, scene transitions), rendering videos to MP4/WebM, or setting up standalone Remotion projects.
Quick Facts
| Field | Value |
|---|---|
| Category | video |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill remotion-video- The remotion-video skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Remotion is a framework for creating videos programmatically using React. Instead of timeline-based editors, you write compositions as React components where every frame is a function of the current frame number. This gives you the full power of TypeScript, npm packages, and component-based architecture for building videos - from animated explainers and social media clips to data-driven visualizations and personalized video at scale.
This skill covers project setup, composition structure, frame-based animations with interpolate and spring, scene sequencing, asset management, audio integration, parametrized videos with Zod schemas, and rendering to MP4/WebM via CLI or programmatic APIs.
Tags
remotion video animation react rendering programmatic-video
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair remotion-video with these complementary skills:
Frequently Asked Questions
What is remotion-video?
Use this skill when creating programmatic videos with Remotion, building React-based video compositions, implementing animations (text, element, scene transitions), rendering videos to MP4/WebM, or setting up standalone Remotion projects. Triggers on video creation, Remotion compositions, useCurrentFrame, interpolate, spring animations, and video rendering.
How do I install remotion-video?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill remotion-video in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support remotion-video?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Remotion Video
Remotion is a framework for creating videos programmatically using React. Instead of timeline-based editors, you write compositions as React components where every frame is a function of the current frame number. This gives you the full power of TypeScript, npm packages, and component-based architecture for building videos - from animated explainers and social media clips to data-driven visualizations and personalized video at scale.
This skill covers project setup, composition structure, frame-based animations with interpolate and spring, scene sequencing, asset management, audio integration, parametrized videos with Zod schemas, and rendering to MP4/WebM via CLI or programmatic APIs.
When to use this skill
Trigger this skill when the user:
- Wants to create a video programmatically using React/TypeScript
- Asks about Remotion compositions, useCurrentFrame, or useVideoConfig
- Needs to animate text, elements, or transitions between scenes
- Wants to render a video to MP4, WebM, or GIF from code
- Asks about spring animations, interpolate, or frame-based timing
- Needs to set up a new Remotion project from scratch
- Wants to add audio, images, or fonts to a Remotion video
- Asks about parametrized/data-driven video generation
- Needs to configure Remotion Studio for previewing compositions
Do NOT trigger this skill for:
- General React questions unrelated to video creation - use React skills
- Video editing with traditional timeline tools (Premiere, DaVinci, FFmpeg CLI)
- CSS animations for web pages - use animation/motion-design skills
- Video playback or streaming in web apps - use media player skills
Key principles
Every frame is a pure function - A Remotion component receives the current frame via
useCurrentFrame()and must render deterministically for that frame. No side effects, no randomness without seeds, no reliance on wall-clock time. The same frame number must always produce the same visual output.Compositions are the unit of video - Each
<Composition>defines a video with explicit dimensions (width, height), frame rate (fps), and duration (durationInFrames). Think of compositions as "pages" in your project - one per video variant or scene that can be rendered independently.Interpolate for everything - The
interpolate()function maps frame numbers to any numeric value (opacity, position, scale, color channels). Combined withextrapolateRight: 'clamp', it is the workhorse for all animations. Usespring()when you need physics-based easing.Sequence for time offsets - Use
<Sequence from={frame}>to delay when children start appearing, and<Series>to play children one after another. Never use setTimeout or manual frame math for sequencing - the declarative primitives handle it correctly across preview and render.Assets are static, data is dynamic - Put images, fonts, and audio files in the
public/folder and reference them withstaticFile(). For dynamic data (API responses, database records), usedelayRender()/continueRender()to pause rendering until the data is loaded.
Core concepts
Composition structure
Every Remotion project has a root file that registers compositions:
import { Composition } from 'remotion';
import { MyVideo } from './MyVideo';
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="MyVideo"
component={MyVideo}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
/>
);
};Frame-based timing
All timing in Remotion is expressed in frames, not seconds. To convert:
- Seconds to frames:
seconds * fps - Frames to seconds:
frame / fps
At 30 fps, a 5-second video is 150 frames. Frame 0 is the first frame.
Animation model
Remotion provides two core animation primitives:
| Primitive | Use case | Example |
|---|---|---|
interpolate() |
Linear/clamped mapping from frame to value | Fade, slide, scale |
spring() |
Physics-based animation with damping/mass | Bouncy entrances, natural motion |
Both return a number you apply to styles (opacity, transform, etc.).
Common tasks
1. Set up a new Remotion project
npx create-video@latestThis scaffolds a project with TypeScript, a sample composition, and Remotion Studio configured. The project structure will be:
my-video/
src/
Root.tsx # Registers all compositions
MyComp.tsx # Your first composition component
public/ # Static assets (images, fonts, audio)
remotion.config.ts # Remotion configuration
package.jsonStart the preview studio with:
npx remotion studio2. Create a basic composition with useCurrentFrame
import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate } from 'remotion';
export const FadeInText: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: 'clamp',
});
const translateY = interpolate(frame, [0, 30], [20, 0], {
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill
style={{
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0f0f0f',
}}
>
<h1
style={{
fontSize: 80,
color: 'white',
opacity,
transform: `translateY(${translateY}px)`,
}}
>
Hello Remotion
</h1>
</AbsoluteFill>
);
};3. Animate text word by word
Split text into words and stagger each word's appearance using <Sequence>:
import { AbsoluteFill, Sequence, useCurrentFrame, interpolate } from 'remotion';
const AnimatedWord: React.FC<{ children: string }> = ({ children }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 15], [0, 1], {
extrapolateRight: 'clamp',
});
const translateY = interpolate(frame, [0, 15], [10, 0], {
extrapolateRight: 'clamp',
});
return (
<span
style={{
display: 'inline-block',
opacity,
transform: `translateY(${translateY}px)`,
marginRight: 12,
}}
>
{children}
</span>
);
};
export const WordByWord: React.FC<{ text: string }> = ({ text }) => {
const words = text.split(' ');
return (
<AbsoluteFill
style={{
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a2e',
flexWrap: 'wrap',
padding: 100,
}}
>
{words.map((word, i) => (
<Sequence key={i} from={i * 8}>
<AnimatedWord>{word}</AnimatedWord>
</Sequence>
))}
</AbsoluteFill>
);
};4. Element animations with spring
Use spring() for physics-based animations that feel natural:
import { AbsoluteFill, useCurrentFrame, useVideoConfig, spring } from 'remotion';
export const SpringCard: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
frame,
fps,
config: { damping: 200, stiffness: 100, mass: 0.5 },
});
const slideUp = spring({
frame: frame - 10,
fps,
config: { damping: 12, stiffness: 100 },
});
return (
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', backgroundColor: '#0d1117' }}>
<div
style={{
width: 400,
height: 250,
backgroundColor: '#161b22',
borderRadius: 16,
transform: `scale(${scale}) translateY(${(1 - slideUp) * 50}px)`,
}}
>
<p style={{ color: 'white', fontSize: 32 }}>Spring Animation</p>
</div>
</AbsoluteFill>
);
};5. Scene transitions with Sequence and Series
Use <Sequence> for overlapping scenes and <Series> for sequential playback:
import { AbsoluteFill, Sequence, Series, useCurrentFrame, interpolate } from 'remotion';
const Scene: React.FC<{ color: string; title: string }> = ({ color, title }) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{ backgroundColor: color, justifyContent: 'center', alignItems: 'center' }}>
<h1 style={{ color: 'white', fontSize: 72, opacity }}>{title}</h1>
</AbsoluteFill>
);
};
export const MultiScene: React.FC = () => {
return (
<AbsoluteFill>
<Series>
<Series.Sequence durationInFrames={60}>
<Scene color="#e63946" title="Scene One" />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<Scene color="#457b9d" title="Scene Two" />
</Series.Sequence>
<Series.Sequence durationInFrames={60}>
<Scene color="#2a9d8f" title="Scene Three" />
</Series.Sequence>
</Series>
</AbsoluteFill>
);
};6. Asset handling (images, fonts, staticFile)
Reference static assets from the public/ folder using staticFile():
import { AbsoluteFill, Img, staticFile } from 'remotion';
export const AssetDemo: React.FC = () => {
return (
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
<Img
src={staticFile('logo.png')}
style={{ width: 300, height: 300 }}
/>
</AbsoluteFill>
);
};Load custom fonts with @remotion/google-fonts or CSS @font-face:
import { loadFont } from '@remotion/google-fonts/Inter';
const { fontFamily } = loadFont();
export const FontDemo: React.FC = () => {
return (
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
<h1 style={{ fontFamily, fontSize: 64, color: 'white' }}>
Custom Font
</h1>
</AbsoluteFill>
);
};7. Audio integration
Add audio with volume control and timing:
import { AbsoluteFill, Audio, Sequence, staticFile, interpolate } from 'remotion';
export const WithAudio: React.FC = () => {
return (
<AbsoluteFill style={{ backgroundColor: '#000' }}>
<Audio
src={staticFile('background-music.mp3')}
volume={(f) =>
interpolate(f, [0, 30, 120, 150], [0, 0.8, 0.8, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
})
}
/>
<Sequence from={15}>
<Audio src={staticFile('whoosh.mp3')} volume={0.5} />
</Sequence>
</AbsoluteFill>
);
};8. Rendering videos
Render to MP4 from the command line:
# Render default composition
npx remotion render src/index.ts MyVideo out/video.mp4
# Render at 4K resolution
npx remotion render src/index.ts MyVideo out/video.mp4 --width 3840 --height 2160
# Render to WebM
npx remotion render src/index.ts MyVideo out/video.webm --codec vp8
# Render a specific frame range
npx remotion render src/index.ts MyVideo out/video.mp4 --frames 0-90
# Render with custom props
npx remotion render src/index.ts MyVideo out/video.mp4 --props '{"title": "Hello"}'For programmatic rendering, see references/rendering-guide.md.
Anti-patterns / common mistakes
| Mistake | Why it is wrong | What to do instead |
|---|---|---|
Using Math.random() without a seed |
Produces different output per frame during render | Use a deterministic seed or random() from Remotion |
Using setTimeout / setInterval |
Breaks frame-based rendering - timers do not advance per frame | Use useCurrentFrame() and frame math for all timing |
Missing extrapolateRight: 'clamp' |
Values overshoot beyond the target range on later frames | Always add { extrapolateRight: 'clamp' } to interpolate() |
| Hardcoding fps in frame calculations | Breaks when composition fps changes | Use useVideoConfig().fps to derive timing |
Using <Video> for rendered output |
Slower rendering performance due to seeking overhead | Use <OffthreadVideo> for better render performance |
Forgetting delayRender() for async data |
Renders before data loads, showing empty/broken frames | Call delayRender() immediately, continueRender() when ready |
| Inline styles with non-deterministic values | Flickers or inconsistency between preview and render | Derive all style values from the frame number only |
| Giant single composition | Hard to maintain and impossible to render scenes independently | Split into multiple compositions or use Series for scenes |
Gotchas
extrapolateRight default is 'extend' - If you write
interpolate(frame, [0, 30], [0, 1])without clamping, the value will keep increasing beyond 1 after frame 30 (e.g., frame 60 gives opacity 2). Always pass{ extrapolateRight: 'clamp' }unless you intentionally want extrapolation.spring() starts from frame 0 of its context - When using
spring()inside a<Sequence from={60}>, the frame passed to spring resets to 0 at frame 60 of the parent. If you pass the parent's raw frame, the animation will already be complete. UseuseCurrentFrame()inside the Sequence child, not a frame from the parent.staticFile() paths are relative to public/ - Calling
staticFile('images/logo.png')looks forpublic/images/logo.png. Using absolute paths or paths outsidepublic/will fail silently in preview and error during render. Ensure all assets are in thepublic/directory.delayRender has a 30-second timeout - If your async operation (API call, font load) takes longer than 30 seconds, the render will abort. Increase the timeout with
delayRender('Loading data', { timeoutInMilliseconds: 60000 })for slow operations.Composition dimensions must be even numbers - Video codecs (H.264, VP8) require even width and height. Remotion will throw an error if you use odd dimensions like 1921x1081. Always use even pixel values (1920x1080, 1280x720, etc.).
References
For detailed patterns on specific Remotion sub-domains, read the relevant file
from the references/ folder:
references/animation-patterns.md- advanced animation techniques including staggered cascades, parallax effects, morph transitions, easing curves, and complex multi-property animationsreferences/rendering-guide.md- rendering configuration, codec options, Lambda/cloud rendering, programmatic rendering API, GIF output, and performance optimizationreferences/project-structure.md- project organization, parametrized videos with Zod schemas, reusable component patterns, shared styles, and multi- composition architecture
Only load a references file if the current task requires it - they are long and will consume context.
References
animation-patterns.md
Animation Patterns
Advanced animation techniques for Remotion compositions.
Staggered cascade animation
Animate a list of items appearing one after another with a delay between each:
import { AbsoluteFill, Sequence, useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
interface CascadeItemProps {
label: string;
}
const CascadeItem: React.FC<CascadeItemProps> = ({ label }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({
frame,
fps,
config: { damping: 12, stiffness: 100 },
});
const opacity = interpolate(progress, [0, 1], [0, 1]);
const translateX = interpolate(progress, [0, 1], [-40, 0]);
return (
<div
style={{
opacity,
transform: `translateX(${translateX}px)`,
fontSize: 36,
color: 'white',
padding: '8px 0',
}}
>
{label}
</div>
);
};
export const StaggeredList: React.FC<{ items: string[] }> = ({ items }) => {
return (
<AbsoluteFill
style={{
justifyContent: 'center',
padding: 100,
backgroundColor: '#1a1a2e',
}}
>
{items.map((item, i) => (
<Sequence key={i} from={i * 10}>
<CascadeItem label={item} />
</Sequence>
))}
</AbsoluteFill>
);
};The key pattern: map over items with <Sequence from={i * delay}> where delay
controls stagger timing. Inside each Sequence, useCurrentFrame() resets to 0.
Parallax scroll effect
Create depth by moving layers at different speeds:
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
interface ParallaxLayerProps {
speed: number;
children: React.ReactNode;
}
const ParallaxLayer: React.FC<ParallaxLayerProps> = ({ speed, children }) => {
const frame = useCurrentFrame();
const translateY = interpolate(frame, [0, 150], [0, -200 * speed], {
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill style={{ transform: `translateY(${translateY}px)` }}>
{children}
</AbsoluteFill>
);
};
export const ParallaxScene: React.FC = () => {
return (
<AbsoluteFill style={{ backgroundColor: '#0d1117' }}>
<ParallaxLayer speed={0.3}>
<div style={{ position: 'absolute', top: 400, left: 100 }}>
<div style={{ width: 200, height: 200, backgroundColor: '#1f2937', borderRadius: 16 }} />
</div>
</ParallaxLayer>
<ParallaxLayer speed={0.7}>
<div style={{ position: 'absolute', top: 300, left: 400 }}>
<div style={{ width: 150, height: 150, backgroundColor: '#374151', borderRadius: 12 }} />
</div>
</ParallaxLayer>
<ParallaxLayer speed={1.0}>
<AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center' }}>
<h1 style={{ color: 'white', fontSize: 72 }}>Parallax</h1>
</AbsoluteFill>
</ParallaxLayer>
</AbsoluteFill>
);
};Higher speed values move faster, creating foreground elements. Lower values
create the illusion of distant background elements.
Morph transition between scenes
Cross-fade between two scenes with a shared transform:
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
interface MorphTransitionProps {
sceneA: React.ReactNode;
sceneB: React.ReactNode;
transitionStart: number;
transitionDuration: number;
}
export const MorphTransition: React.FC<MorphTransitionProps> = ({
sceneA,
sceneB,
transitionStart,
transitionDuration,
}) => {
const frame = useCurrentFrame();
const transitionEnd = transitionStart + transitionDuration;
const progress = interpolate(frame, [transitionStart, transitionEnd], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const scaleA = interpolate(progress, [0, 1], [1, 1.2]);
const scaleB = interpolate(progress, [0, 1], [0.8, 1]);
return (
<AbsoluteFill>
<AbsoluteFill style={{ opacity: 1 - progress, transform: `scale(${scaleA})` }}>
{sceneA}
</AbsoluteFill>
<AbsoluteFill style={{ opacity: progress, transform: `scale(${scaleB})` }}>
{sceneB}
</AbsoluteFill>
</AbsoluteFill>
);
};Custom easing curves
Create custom easing by composing interpolate with an easing function:
import { useCurrentFrame, interpolate, Easing } from 'remotion';
export const EasingDemo: React.FC = () => {
const frame = useCurrentFrame();
// Ease in-out cubic
const easeInOut = interpolate(frame, [0, 60], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.inOut(Easing.cubic),
});
// Bounce
const bounce = interpolate(frame, [0, 60], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.bounce,
});
// Elastic
const elastic = interpolate(frame, [0, 60], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.elastic(1),
});
// Bezier curve
const bezier = interpolate(frame, [0, 60], [0, 1], {
extrapolateRight: 'clamp',
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
});
return null; // Apply these values to styles
};Available easing functions from Easing:
Easing.linear- constant speedEasing.ease- default CSS easeEasing.cubic/Easing.quad/Easing.sin- power curvesEasing.bounce- bouncing ball effectEasing.elastic(bounciness)- spring overshootEasing.bezier(x1, y1, x2, y2)- custom cubic bezierEasing.in(fn)/Easing.out(fn)/Easing.inOut(fn)- directional wrappers
Multi-property animation
Animate multiple properties with different timings from a single frame source:
import { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig } from 'remotion';
export const MultiProperty: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Phase 1: Fade in (frames 0-20)
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateRight: 'clamp',
});
// Phase 2: Scale up with spring (frames 10-40)
const scale = spring({
frame: Math.max(0, frame - 10),
fps,
config: { damping: 10, stiffness: 100 },
});
// Phase 3: Slide to final position (frames 20-50)
const translateX = interpolate(frame, [20, 50], [-100, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
// Phase 4: Color transition (frames 30-60)
const colorProgress = interpolate(frame, [30, 60], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
});
const r = Math.round(interpolate(colorProgress, [0, 1], [255, 99]));
const g = Math.round(interpolate(colorProgress, [0, 1], [255, 102]));
const b = Math.round(interpolate(colorProgress, [0, 1], [255, 241]));
return (
<AbsoluteFill
style={{
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0f0f0f',
}}
>
<div
style={{
opacity,
transform: `scale(${scale}) translateX(${translateX}px)`,
color: `rgb(${r}, ${g}, ${b})`,
fontSize: 64,
fontWeight: 'bold',
}}
>
Multi-Phase Animation
</div>
</AbsoluteFill>
);
};Pattern: use different frame ranges for each property. Offset start frames to
create overlapping phases. Use Math.max(0, frame - offset) for spring delays.
Typewriter effect
Reveal text character by character:
import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion';
interface TypewriterProps {
text: string;
charsPerFrame?: number;
}
export const Typewriter: React.FC<TypewriterProps> = ({
text,
charsPerFrame = 0.5,
}) => {
const frame = useCurrentFrame();
const charsToShow = Math.floor(frame * charsPerFrame);
const displayText = text.slice(0, charsToShow);
const showCursor = frame % 15 < 10;
return (
<AbsoluteFill
style={{
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1e1e1e',
}}
>
<p
style={{
fontFamily: 'monospace',
fontSize: 48,
color: '#d4d4d4',
}}
>
{displayText}
<span style={{ opacity: showCursor ? 1 : 0 }}>|</span>
</p>
</AbsoluteFill>
);
};Circular motion
Animate elements along a circular path:
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
interface OrbiterProps {
radius: number;
speed: number;
size: number;
color: string;
}
const Orbiter: React.FC<OrbiterProps> = ({ radius, speed, size, color }) => {
const frame = useCurrentFrame();
const angle = interpolate(frame, [0, 90], [0, Math.PI * 2 * speed]);
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
return (
<div
style={{
position: 'absolute',
width: size,
height: size,
borderRadius: '50%',
backgroundColor: color,
transform: `translate(${x}px, ${y}px)`,
left: '50%',
top: '50%',
marginLeft: -size / 2,
marginTop: -size / 2,
}}
/>
);
};
export const CircularMotion: React.FC = () => {
return (
<AbsoluteFill style={{ backgroundColor: '#0a0a0a' }}>
<Orbiter radius={200} speed={1} size={30} color="#e63946" />
<Orbiter radius={150} speed={-0.7} size={20} color="#457b9d" />
<Orbiter radius={100} speed={1.5} size={15} color="#2a9d8f" />
</AbsoluteFill>
);
};Key: use Math.cos and Math.sin with a frame-derived angle. Multiply speed
to control rotation rate. Negative speed reverses direction.
project-structure.md
Project Structure
Organizing Remotion projects for maintainability and reuse.
Standard project layout
my-video/
src/
Root.tsx # Registers all compositions
index.ts # Entry point (exports Root)
compositions/
Intro.tsx # Individual composition components
MainContent.tsx
Outro.tsx
components/
AnimatedText.tsx # Reusable animation components
Background.tsx
Logo.tsx
hooks/
useAnimation.ts # Custom animation hooks
useData.ts # Data fetching hooks
lib/
colors.ts # Shared constants
fonts.ts # Font loading
schemas.ts # Zod schemas for input props
types.ts # TypeScript type definitions
public/
fonts/ # Custom font files
images/ # Static images
audio/ # Music and sound effects
remotion.config.ts # Remotion CLI configuration
package.json
tsconfig.jsonEntry point and Root
The entry point exports the Root component that registers all compositions:
// src/index.ts
import { registerRoot } from 'remotion';
import { RemotionRoot } from './Root';
registerRoot(RemotionRoot);// src/Root.tsx
import { Composition } from 'remotion';
import { Intro } from './compositions/Intro';
import { MainContent } from './compositions/MainContent';
import { Outro } from './compositions/Outro';
import { introSchema, mainSchema, outroSchema } from './lib/schemas';
export const RemotionRoot: React.FC = () => {
return (
<>
<Composition
id="Intro"
component={Intro}
durationInFrames={90}
fps={30}
width={1920}
height={1080}
schema={introSchema}
defaultProps={{
title: 'Welcome',
subtitle: 'An example video',
}}
/>
<Composition
id="MainContent"
component={MainContent}
durationInFrames={300}
fps={30}
width={1920}
height={1080}
schema={mainSchema}
defaultProps={{
items: ['Item 1', 'Item 2', 'Item 3'],
}}
/>
<Composition
id="Outro"
component={Outro}
durationInFrames={60}
fps={30}
width={1920}
height={1080}
schema={outroSchema}
defaultProps={{
message: 'Thanks for watching',
}}
/>
</>
);
};Parametrized videos with Zod schemas
Define input props with Zod for type safety and Remotion Studio UI generation:
// src/lib/schemas.ts
import { z } from 'zod';
export const introSchema = z.object({
title: z.string().describe('Main title text'),
subtitle: z.string().describe('Subtitle text below the title'),
backgroundColor: z.string().default('#0f0f0f').describe('Background color'),
accentColor: z.string().default('#e63946').describe('Accent color for highlights'),
});
export const mainSchema = z.object({
items: z.array(z.string()).describe('List of items to display'),
showNumbers: z.boolean().default(true).describe('Show item numbers'),
animationSpeed: z.enum(['slow', 'normal', 'fast']).default('normal'),
});
export const outroSchema = z.object({
message: z.string().describe('Closing message'),
showLogo: z.boolean().default(true),
});
export type IntroProps = z.infer<typeof introSchema>;
export type MainProps = z.infer<typeof mainSchema>;
export type OutroProps = z.infer<typeof outroSchema>;Use the schema in compositions:
// src/compositions/Intro.tsx
import { AbsoluteFill, useCurrentFrame, interpolate } from 'remotion';
import type { IntroProps } from '../lib/schemas';
export const Intro: React.FC<IntroProps> = ({
title,
subtitle,
backgroundColor,
accentColor,
}) => {
const frame = useCurrentFrame();
const titleOpacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateRight: 'clamp',
});
const subtitleOpacity = interpolate(frame, [20, 50], [0, 1], {
extrapolateRight: 'clamp',
});
return (
<AbsoluteFill
style={{
backgroundColor,
justifyContent: 'center',
alignItems: 'center',
}}
>
<h1 style={{ color: accentColor, fontSize: 80, opacity: titleOpacity }}>
{title}
</h1>
<p style={{ color: 'white', fontSize: 36, opacity: subtitleOpacity }}>
{subtitle}
</p>
</AbsoluteFill>
);
};When a schema is set on a <Composition>, Remotion Studio renders form
controls in the sidebar for each field, using describe() values as labels.
Reusable animation components
Build a library of composable animation primitives:
// src/components/FadeIn.tsx
import { useCurrentFrame, interpolate } from 'remotion';
interface FadeInProps {
children: React.ReactNode;
durationFrames?: number;
direction?: 'up' | 'down' | 'left' | 'right' | 'none';
distance?: number;
}
export const FadeIn: React.FC<FadeInProps> = ({
children,
durationFrames = 20,
direction = 'up',
distance = 30,
}) => {
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, durationFrames], [0, 1], {
extrapolateRight: 'clamp',
});
const translate = interpolate(frame, [0, durationFrames], [distance, 0], {
extrapolateRight: 'clamp',
});
const transforms: Record<string, string> = {
up: `translateY(${translate}px)`,
down: `translateY(${-translate}px)`,
left: `translateX(${translate}px)`,
right: `translateX(${-translate}px)`,
none: 'none',
};
return (
<div style={{ opacity, transform: transforms[direction] }}>
{children}
</div>
);
};// src/components/ScaleIn.tsx
import { useCurrentFrame, useVideoConfig, spring } from 'remotion';
interface ScaleInProps {
children: React.ReactNode;
delay?: number;
damping?: number;
}
export const ScaleIn: React.FC<ScaleInProps> = ({
children,
delay = 0,
damping = 12,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const scale = spring({
frame: Math.max(0, frame - delay),
fps,
config: { damping },
});
return (
<div style={{ transform: `scale(${scale})` }}>
{children}
</div>
);
};Usage in compositions:
import { Sequence } from 'remotion';
import { FadeIn } from '../components/FadeIn';
import { ScaleIn } from '../components/ScaleIn';
export const MyScene: React.FC = () => {
return (
<AbsoluteFill>
<Sequence from={0}>
<FadeIn direction="up" durationFrames={30}>
<h1>Title</h1>
</FadeIn>
</Sequence>
<Sequence from={20}>
<ScaleIn damping={8}>
<img src={staticFile('logo.png')} />
</ScaleIn>
</Sequence>
</AbsoluteFill>
);
};Font loading
Centralize font loading for consistency:
// src/lib/fonts.ts
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
import { loadFont as loadPoppins } from '@remotion/google-fonts/Poppins';
export const inter = loadInter();
export const poppins = loadPoppins();For custom fonts, place .woff2 files in public/fonts/ and use CSS
@font-face in your components.
Shared style constants
// src/lib/colors.ts
export const colors = {
background: '#0f0f0f',
surface: '#1a1a2e',
primary: '#e63946',
secondary: '#457b9d',
accent: '#2a9d8f',
text: '#ffffff',
textMuted: '#a0a0a0',
} as const;
export const sizes = {
titleFont: 80,
subtitleFont: 48,
bodyFont: 32,
padding: 100,
borderRadius: 16,
} as const;
export const timing = {
fadeIn: 20,
fadeOut: 15,
staggerDelay: 8,
sceneTransition: 30,
} as const;Async data loading pattern
Load external data before rendering:
// src/hooks/useData.ts
import { useCallback, useEffect, useState } from 'react';
import { continueRender, delayRender } from 'remotion';
export function useAsyncData<T>(fetcher: () => Promise<T>) {
const [data, setData] = useState<T | null>(null);
const [handle] = useState(() => delayRender('Loading async data'));
const fetchData = useCallback(async () => {
try {
const result = await fetcher();
setData(result);
continueRender(handle);
} catch (err) {
console.error('Failed to load data:', err);
continueRender(handle);
}
}, [fetcher, handle]);
useEffect(() => {
fetchData();
}, [fetchData]);
return data;
}Usage:
import { AbsoluteFill } from 'remotion';
import { useAsyncData } from '../hooks/useData';
export const DataDriven: React.FC = () => {
const data = useAsyncData(async () => {
const response = await fetch('https://api.example.com/stats');
return response.json();
});
if (!data) {
return null; // Will not render until data loads
}
return (
<AbsoluteFill>
<h1 style={{ color: 'white', fontSize: 64 }}>{data.title}</h1>
</AbsoluteFill>
);
};The delayRender() call pauses the renderer until continueRender() is called,
ensuring async data is available before any frame is captured.
rendering-guide.md
Rendering Guide
Comprehensive guide to rendering Remotion compositions to video files.
CLI rendering
The primary way to render videos is the Remotion CLI:
# Basic render to MP4 (H.264 codec)
npx remotion render src/index.ts MyComposition out/video.mp4
# Specify codec explicitly
npx remotion render src/index.ts MyComposition out/video.mp4 --codec h264
# Render to WebM (VP8)
npx remotion render src/index.ts MyComposition out/video.webm --codec vp8
# Render to ProRes (for editing in Final Cut/Premiere)
npx remotion render src/index.ts MyComposition out/video.mov --codec prores
# Render a GIF
npx remotion render src/index.ts MyComposition out/animation.gif --codec gifResolution and quality options
# Custom resolution (overrides composition defaults)
npx remotion render src/index.ts MyComposition out/video.mp4 --width 3840 --height 2160
# Control CRF (quality) - lower is better quality, higher file size
# H.264 range: 1-51, default 18
npx remotion render src/index.ts MyComposition out/video.mp4 --crf 15
# Frame range (render only specific frames)
npx remotion render src/index.ts MyComposition out/video.mp4 --frames 0-90
# Specific single frame as image
npx remotion still src/index.ts MyComposition out/thumbnail.png --frame 45Passing input props
# Pass JSON props to the composition
npx remotion render src/index.ts MyComposition out/video.mp4 \
--props '{"title": "Hello World", "color": "#ff0000"}'
# Pass props from a file
npx remotion render src/index.ts MyComposition out/video.mp4 \
--props ./input-data.jsonCodec comparison
| Codec | Format | Quality | File Size | Use Case |
|---|---|---|---|---|
h264 |
MP4 | Excellent | Medium | Web, social media, general distribution |
h265 |
MP4 | Excellent | Smaller | When H.265 support is available |
vp8 |
WebM | Good | Medium | Web embedding, open format |
vp9 |
WebM | Excellent | Smaller | High-quality web embedding |
prores |
MOV | Lossless | Very Large | Professional editing (Final Cut, Premiere) |
gif |
GIF | Low (256 colors) | Large | Short loops, previews |
Recommendation: Use h264 for most distribution. Use prores when the output
will be edited further. Use gif only for very short clips (under 5 seconds).
Programmatic rendering
Render from Node.js code for batch processing or server-side rendering:
import { bundle } from '@remotion/bundler';
import {
renderMedia,
selectComposition,
getCompositions,
} from '@remotion/renderer';
import path from 'path';
async function renderVideo() {
// Step 1: Bundle the project
const bundleLocation = await bundle({
entryPoint: path.resolve('./src/index.ts'),
webpackOverride: (config) => config,
});
// Step 2: Select composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id: 'MyComposition',
inputProps: {
title: 'Dynamic Title',
},
});
// Step 3: Render
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: path.resolve('./out/video.mp4'),
inputProps: {
title: 'Dynamic Title',
},
onProgress: ({ progress }) => {
console.log(`Rendering: ${Math.round(progress * 100)}%`);
},
});
console.log('Render complete');
}
renderVideo();Batch rendering multiple compositions
import { bundle } from '@remotion/bundler';
import { getCompositions, renderMedia } from '@remotion/renderer';
import path from 'path';
async function renderAll() {
const bundleLocation = await bundle({
entryPoint: path.resolve('./src/index.ts'),
});
const compositions = await getCompositions(bundleLocation);
for (const composition of compositions) {
console.log(`Rendering ${composition.id}...`);
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: path.resolve(`./out/${composition.id}.mp4`),
});
}
console.log(`Rendered ${compositions.length} videos`);
}
renderAll();Rendering with dynamic data
import { bundle } from '@remotion/bundler';
import { renderMedia, selectComposition } from '@remotion/renderer';
import path from 'path';
interface VideoData {
userName: string;
score: number;
avatarUrl: string;
}
async function renderPersonalizedVideo(data: VideoData) {
const bundleLocation = await bundle({
entryPoint: path.resolve('./src/index.ts'),
});
const composition = await selectComposition({
serveUrl: bundleLocation,
id: 'PersonalizedVideo',
inputProps: data,
});
const outputPath = path.resolve(`./out/${data.userName}.mp4`);
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: outputPath,
inputProps: data,
});
return outputPath;
}Rendering still images
Export a single frame as PNG or JPEG:
import { bundle } from '@remotion/bundler';
import { renderStill, selectComposition } from '@remotion/renderer';
import path from 'path';
async function renderThumbnail() {
const bundleLocation = await bundle({
entryPoint: path.resolve('./src/index.ts'),
});
const composition = await selectComposition({
serveUrl: bundleLocation,
id: 'MyComposition',
});
await renderStill({
composition,
serveUrl: bundleLocation,
output: path.resolve('./out/thumbnail.png'),
frame: 45,
imageFormat: 'png',
});
}
renderThumbnail();CLI equivalent:
npx remotion still src/index.ts MyComposition out/thumbnail.png --frame 45
npx remotion still src/index.ts MyComposition out/thumbnail.jpeg --frame 45 --image-format jpeg --quality 90Performance optimization
Parallel rendering with concurrency
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: './out/video.mp4',
concurrency: 4, // Render 4 frames in parallel
});Default concurrency is 50% of CPU cores. Increase for CPU-heavy compositions, decrease if running out of memory.
Use OffthreadVideo for embedded video clips
import { OffthreadVideo, staticFile } from 'remotion';
// Preferred: better rendering performance
export const GoodVideoEmbed: React.FC = () => {
return <OffthreadVideo src={staticFile('clip.mp4')} />;
};
// Avoid: slower rendering due to seeking
export const SlowVideoEmbed: React.FC = () => {
return <video src={staticFile('clip.mp4')} />;
};<OffthreadVideo> extracts frames outside the browser, significantly reducing
render time for compositions that include video clips.
Prefetch remote assets
import { prefetch } from 'remotion';
const { free, waitUntilDone } = prefetch('https://example.com/large-image.png', {
method: 'blob-url',
});
// Use in component after prefetch completesReduce bundle size
// remotion.config.ts
import { Config } from '@remotion/cli/config';
Config.overrideWebpackConfig((config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve?.alias,
// Tree-shake unused heavy dependencies
},
},
};
});Remotion Lambda (cloud rendering)
Render videos in AWS Lambda for horizontal scaling:
import { renderMediaOnLambda } from '@remotion/lambda/client';
const result = await renderMediaOnLambda({
region: 'us-east-1',
functionName: 'remotion-render',
serveUrl: 'https://your-site.com/bundle',
composition: 'MyComposition',
codec: 'h264',
inputProps: { title: 'Cloud Rendered' },
});
console.log('Video URL:', result.url);Lambda rendering splits the video into chunks, renders each chunk in a separate Lambda invocation, then stitches them together. This allows rendering a 5-minute video in under 30 seconds.
Setup requirements:
- Deploy a Lambda function with
npx remotion lambda functions deploy - Deploy the Remotion bundle with
npx remotion lambda sites create - Configure AWS credentials with appropriate IAM permissions
Configuration file
remotion.config.ts at the project root:
import { Config } from '@remotion/cli/config';
// Set default codec
Config.setCodec('h264');
// Set default CRF (quality)
Config.setCrf(18);
// Set concurrency
Config.setConcurrency(4);
// Override webpack config
Config.overrideWebpackConfig((config) => {
return {
...config,
module: {
...config.module,
rules: [
...(config.module?.rules ?? []),
// Add custom loaders here
],
},
};
});Troubleshooting rendering issues
| Issue | Cause | Solution |
|---|---|---|
| Blank frames | Async data not loaded | Use delayRender() / continueRender() |
| Flickering | Non-deterministic rendering | Remove Math.random(), use seeded values |
| Out of memory | Too many concurrent frames | Reduce concurrency option |
| Slow render | Using <Video> component |
Switch to <OffthreadVideo> |
| Missing assets | Wrong staticFile() path |
Verify file exists in public/ folder |
| Codec error | Odd dimensions | Ensure width and height are even numbers |
| Audio desync | Wrong sample rate | Use 44100 or 48000 Hz audio files |
Frequently Asked Questions
What is remotion-video?
Use this skill when creating programmatic videos with Remotion, building React-based video compositions, implementing animations (text, element, scene transitions), rendering videos to MP4/WebM, or setting up standalone Remotion projects. Triggers on video creation, Remotion compositions, useCurrentFrame, interpolate, spring animations, and video rendering.
How do I install remotion-video?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill remotion-video in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support remotion-video?
remotion-video works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.