Skip to main content ホーム クリエイター inugamidev ultrathink-oss video-studio
video-studio Unified video generation studio — Remotion framework (setup, compositions, Player, rendering), Remotion video production (scenes, transitions, OffthreadVideo, multi-scene), content scripting (data-driven, captions, CSV/JSON), animation (spring physics, interpolate, 12 Disney animation principles), and broader motion design patterns. Single entry point for any 'make a video' task.
インストールへ移動 Skills Marketplace コミュニティが作成したAIスキルを発見・探索
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
直接コマンドでは確認用 Prompt が省略されます。実行前にソースを確認してください。
npx skills add https://github.com/InugamiDev/ultrathink-oss --skill video-studioコマンドは1行のまま表示されます。コピー前に横へスクロールして全体を確認してください。
ローカルで確認しますか?SkillsMP が現在取得できるファイルをダウンロードできます。
Zipをダウンロード ダウンロード中... このリポジトリの他の Skills Unified design foundations — design system architecture, tokens, component specs, visual principles, creative vision, figma integration, plus brand design system loader (66 real brands via DESIGN.md). Absorbs design, design-system, design-systems, design-principles, design-router, creative-vision, figma, design-md.
Render, summarize, and present markdown documents and structured content in multiple output modes
Ultra UI skill - combines Google's DESIGN.md spec (machine-readable design tokens) with the ui-ux-pro-max knowledge base (91 styles, 161 palettes, 73 font pairings, 161 products, 104 UX guidelines, 25 chart types). Generates lint-clean DESIGN.md files, validates token references and WCAG contrast, exports Tailwind/DTCG tokens, and diffs design systems version-over-version.
name video-studio description Unified video generation studio — Remotion framework (setup, compositions, Player, rendering), Remotion video production (scenes, transitions, OffthreadVideo, multi-scene), content scripting (data-driven, captions, CSV/JSON), animation (spring physics, interpolate, 12 Disney animation principles), and broader motion design patterns. Single entry point for any 'make a video' task. layer domain category frontend triggers ["AnimatePresence","OffthreadVideo","TransitionSeries","batch render","calculateMetadata","continueRender","create video programmatically","data-driven video","delayRender","disney principles","dynamic video","easing curve","embed remotion","frame animation","framer motion","gesture animation","interpolateColors","layout animation","motion design","mp4 from react","page transition","parameterized video","programmatic video","react video","reduced motion","remotion","remotion animation","remotion audio","remotion captions","remotion cloud run","remotion codec","remotion content","remotion data","remotion dataset","remotion easing","remotion entrance","remotion interpolate","remotion lambda","remotion motion","remotion noise","remotion overlay","remotion player","remotion prefetch","remotion project","remotion render","remotion scene","remotion sequence","remotion series","remotion setup","remotion spring","remotion srt","remotion stagger","remotion studio","remotion subtitles","remotion template","remotion text animation","remotion timing","remotion transition","remotion video","remotion whisper","render video","scroll animation","spring animation","spring physics video","tiktok captions","video animation","video composition","video from api","video from code","video from data","video rendering react","video scene","video template","video timeline","video transition effect","webm render"]
video-studio
Unified video generation studio — Remotion framework (setup, compositions, Player, rendering), Remotion video production (scenes, transitions, OffthreadVideo, multi-scene), content scripting (data-driven, captions, CSV/JSON), animation (spring physics, interpolate, 12 Disney animation principles), and broader motion design patterns. Single entry point for any 'make a video' task.
Absorbs
remotion
remotion-video
remotion-content
remotion-animation
motion-design
From remotion
Remotion framework hub — project setup, composition architecture, rendering pipeline, Player embedding, and deployment patterns
Remotion — Framework Hub
Purpose
Hub skill for the Remotion ecosystem. Covers project setup, composition architecture, the rendering pipeline (local, Lambda, Cloud Run), the <Player> component for web embedding, and orchestrates handoff to specialized Remotion skills for video production, content scripting, and animation.
When to Chain
Task Chain To Scene transitions, visual effects, rendering output remotion-videoData-driven videos, captions, templates, dynamic props remotion-contentSpring physics, interpolation, easing, timing remotion-animationFramer Motion for UI (not video) motion-design
Project Setup
New Project
npx create-video@latest my-video
npx create-video@latest --template hello-world
npx create-video@latest --template tiktok
npx create-video@latest --template three
npx create-video@latest --template audiogram
npx remotion studio
Add to Existing React Project
npm i remotion @remotion/cli @remotion/bundler
{ } ;
. ( );
. ( );
import
Config
from
"@remotion/cli/config"
Config
setVideoImageFormat
"jpeg"
Config
setOverwriteOutput
true
Project Structure my-video/
├── src/
│ ├── Root.tsx # Register all compositions
│ ├── compositions/
│ │ ├── MyVideo.tsx # Video component
│ │ └── schema.ts # Zod props schema
│ ├── components/ # Reusable visual components
│ └── lib/ # Utilities, data fetching
├── public/ # Static assets (use staticFile())
├── remotion.config.ts # CLI configuration
└── package.json
Composition Architecture
Root Registration
import { Composition } from "remotion" ;
import { z } from "zod" ;
import { MyVideo } from "./compositions/MyVideo" ;
import { myVideoSchema, calcMyVideoMetadata } from "./compositions/schema" ;
export const RemotionRoot = ( ) => (
<>
<Composition
id ="MyVideo"
component ={MyVideo}
durationInFrames ={300}
fps ={30}
width ={1920}
height ={1080}
schema ={myVideoSchema}
defaultProps ={{
title: "Hello World ",
data: null ,
}}
calculateMetadata ={calcMyVideoMetadata}
/>
{/* Multiple compositions in one project */}
<Composition
id ="Shorts"
component ={MyVideo}
durationInFrames ={450}
fps ={30}
width ={1080}
height ={1920}
defaultProps ={{ title: "Short Video ", data: null }}
/>
</>
);
Composition with Zod Schema
import { z } from "zod" ;
import { CalculateMetadataFunction } from "remotion" ;
export const myVideoSchema = z.object ({
title : z.string (),
data : z.nullable (z.object ({
items : z.array (z.object ({
name : z.string (),
value : z.number (),
})),
duration : z.number ().optional (),
})),
});
type Props = z.infer <typeof myVideoSchema>;
export const calcMyVideoMetadata : CalculateMetadataFunction <Props > = async ({
props,
}) => {
const response = await fetch (`https://api.example.com/video-data` );
const data = await response.json ();
return {
props : { ...props, data },
durationInFrames : data.duration ? data.duration * 30 : 300 ,
};
};
Core Hooks import {
useCurrentFrame,
useVideoConfig,
AbsoluteFill ,
staticFile,
} from "remotion" ;
export const MyVideo : React .FC = () => {
const frame = useCurrentFrame ();
const { fps, durationInFrames, width, height } = useVideoConfig ();
const currentTimeSeconds = frame / fps;
return (
<AbsoluteFill style ={{ backgroundColor: "white " }}>
<h1 > Frame {frame} of {durationInFrames}</h1 >
<p > Time: {currentTimeSeconds.toFixed(2)}s</p >
</AbsoluteFill >
);
};
Rendering Pipeline
Local Rendering
npx remotion render MyVideo out/video.mp4
npx remotion render MyVideo out/video.mp4 --frames=0-90
npx remotion render MyVideo out/video.mp4 --width=1080 --height=1080
npx remotion render MyVideo out/video.gif --codec=gif
npx remotion render MyVideo out/video.webm --codec=vp8
npx remotion render MyVideo out/video.mp4 --props='{"title":"Custom"}'
npx remotion still MyVideo out/thumbnail.png --frame=60
npx remotion render MyVideo out/video.mp4 --concurrency=4
Programmatic Rendering (Node.js) import { bundle } from "@remotion/bundler" ;
import { renderMedia, selectComposition } from "@remotion/renderer" ;
async function render ( ) {
const bundled = await bundle ({
entryPoint : "./src/index.ts" ,
webpackOverride : (config ) => config,
});
const composition = await selectComposition ({
serveUrl : bundled,
id : "MyVideo" ,
inputProps : { title : "Programmatic" },
});
await renderMedia ({
composition,
serveUrl : bundled,
codec : "h264" ,
outputLocation : "out/video.mp4" ,
inputProps : { title : "Programmatic" },
});
}
Lambda Rendering (Serverless)
npx remotion lambda policies role
npx remotion lambda sites create src/index.ts --site-name=my-video
npx remotion lambda render my-video-site MyVideo
import { renderMediaOnLambda } from "@remotion/lambda/client" ;
const { renderId, bucketName } = await renderMediaOnLambda({
region: "us-east-1" ,
functionName: "remotion-render" ,
serveUrl: siteUrl,
composition: "MyVideo" ,
inputProps: { title: "Lambda Render" },
codec: "h264" ,
});
Player Component (Web Embedding) Embed Remotion compositions in any React app for interactive playback without rendering to file.
import { Player } from "@remotion/player" ;
import { MyVideo } from "./compositions/MyVideo" ;
function VideoPreview ( ) {
return (
<Player
component ={MyVideo}
inputProps ={{ title: "Preview " }}
durationInFrames ={300}
fps ={30}
compositionWidth ={1920}
compositionHeight ={1080}
style ={{ width: "100 %", borderRadius: "0.75rem " }}
controls
autoPlay
loop
clickToPlay
// Responsive
renderLoading ={() => <div > Loading...</div > }
/>
);
}
Player with Ref (Programmatic Control) import { Player , PlayerRef } from "@remotion/player" ;
import { useRef, useCallback } from "react" ;
function ControlledPlayer ( ) {
const playerRef = useRef<PlayerRef >(null );
const seekTo = useCallback ((frame : number ) => {
playerRef.current ?.seekTo (frame);
}, []);
return (
<>
<Player
ref ={playerRef}
component ={MyVideo}
inputProps ={{ title: "Controlled " }}
durationInFrames ={300}
fps ={30}
compositionWidth ={1920}
compositionHeight ={1080}
style ={{ width: "100 %" }}
/>
<div className ="flex gap-4 mt-4" >
<button onClick ={() => playerRef.current?.play()}>Play</button >
<button onClick ={() => playerRef.current?.pause()}>Pause</button >
<button onClick ={() => seekTo(0)}>Restart</button >
<button onClick ={() => seekTo(150)}>Jump to 5s</button >
</div >
</>
);
}
Static Assets import { staticFile, Img } from "remotion" ;
const logoUrl = staticFile ("logo.png" );
const fontUrl = staticFile ("fonts/Inter.woff2" );
const audioUrl = staticFile ("bgm.mp3" );
<Img src ={staticFile( "photo.jpg ")} style ={{ width: "100 %" }} />
Font Loading import { staticFile } from "remotion" ;
const fontFamily = "Inter" ;
const fontUrl = staticFile ("fonts/Inter-Bold.woff2" );
const style = `
@font-face {
font-family: '${fontFamily} ';
src: url('${fontUrl} ') format('woff2');
font-weight: 700;
}
` ;
export const WithFont : React .FC = () => (
<>
<style > {style}</style >
<div style ={{ fontFamily }}> Hello with custom font</div >
</>
);
Best Practices
Use <OffthreadVideo> over <Video> — Better rendering performance, extracts frames without blocking.
Schema with Zod — Validate all composition props with Zod schemas for type safety and Remotion Studio UI.
calculateMetadata for data fetching — Fetch data before render starts, not inside components.
staticFile() for assets — Always use staticFile() for files in public/, never relative paths.
<Img> over <img> — Remotion's <Img> delays rendering until loaded, preventing blank frames.
AbsoluteFill as root — Use <AbsoluteFill> as the root of every scene for proper positioning.
Deterministic renders — No Math.random(), no Date.now(). Use frame and props for all values.
Keep compositions pure — No side effects, no async operations inside render. Use delayRender/continueRender for async.
Common Pitfalls Pitfall Fix Using <img> instead of <Img> Blank frames — <Img> waits for load Math.random() in renderNon-deterministic frames — use random(seed) from remotion Importing Remotion in production app Bundle bloat — Remotion is for video rendering only Missing extrapolateRight: "clamp" Values overshoot beyond input range No Zod schema Remotion Studio can't generate prop UI Fetching data in component render Use calculateMetadata or delayRender instead
From remotion-video
Remotion video production — scenes, transitions, OffthreadVideo, audio mixing, multi-scene timelines, and rendering output
Remotion Video Production
Purpose Expert guidance on building multi-scene video compositions with Remotion. Covers scene organization with Sequence/Series/TransitionSeries, transition effects, OffthreadVideo for embedding video files, audio mixing and synchronization, and rendering output configuration.
Scene Organization
Sequence (Manual Timing) Position scenes manually with frame offsets:
import { Sequence , AbsoluteFill } from "remotion" ;
export const MyVideo : React .FC = () => (
<AbsoluteFill >
{/* Intro: frames 0-89 (3 seconds at 30fps) */}
<Sequence from ={0} durationInFrames ={90} >
<IntroScene />
</Sequence >
{/* Main: frames 90-269 (6 seconds) */}
<Sequence from ={90} durationInFrames ={180} >
<MainScene />
</Sequence >
{/* Outro: frames 270-359 (3 seconds) */}
<Sequence from ={270} durationInFrames ={90} >
<OutroScene />
</Sequence >
{/* Background music spans entire video */}
<Sequence from ={0} >
<BackgroundMusic />
</Sequence >
</AbsoluteFill >
);
Series (Sequential, No Overlap) Scenes play back-to-back automatically:
import { Series , AbsoluteFill } from "remotion" ;
export const MyVideo : React .FC = () => (
<AbsoluteFill >
<Series >
<Series.Sequence durationInFrames ={90} >
<IntroScene />
</Series.Sequence >
{/* Optional gap between scenes */}
<Series.Sequence offset ={15} durationInFrames ={180} >
<MainScene />
</Series.Sequence >
<Series.Sequence durationInFrames ={90} >
<OutroScene />
</Series.Sequence >
</Series >
</AbsoluteFill >
);
TransitionSeries (Crossfades & Wipes) Scenes overlap during transitions:
import { AbsoluteFill } from "remotion" ;
import { TransitionSeries , linearTiming, springTiming } from "@remotion/transitions" ;
import { fade } from "@remotion/transitions/fade" ;
import { slide } from "@remotion/transitions/slide" ;
import { wipe } from "@remotion/transitions/wipe" ;
export const MyVideo : React .FC = () => (
<AbsoluteFill >
<TransitionSeries >
<TransitionSeries.Sequence durationInFrames ={90} >
<IntroScene />
</TransitionSeries.Sequence >
{/* Fade transition (30 frames = 1 second) */}
<TransitionSeries.Transition
presentation ={fade()}
timing ={linearTiming({ durationInFrames: 30 })}
/>
<TransitionSeries.Sequence durationInFrames ={180} >
<MainScene />
</TransitionSeries.Sequence >
{/* Slide transition with spring physics */}
<TransitionSeries.Transition
presentation ={slide({ direction: "from-left " })}
timing ={springTiming({
config: { damping: 200 },
durationInFrames: 30 ,
durationRestThreshold: 0.001 ,
})}
/>
<TransitionSeries.Sequence durationInFrames ={90} >
<OutroScene />
</TransitionSeries.Sequence >
</TransitionSeries >
</AbsoluteFill >
);
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" ;
import { none } from "@remotion/transitions/none" ;
fade ()
slide ({ direction : "from-left" })
wipe ({ direction : "from-left" })
flip ({ direction : "from-left" })
clockWipe ({ width : 1920 , height : 1080 })
none ()
Overlay Effects (No Timing Impact) <TransitionSeries >
<TransitionSeries.Sequence durationInFrames ={90} >
<SceneA />
</TransitionSeries.Sequence >
{}
<TransitionSeries .Overlay durationInFrames={20 }>
<LightLeakEffect />
</TransitionSeries .Overlay >
<TransitionSeries.Sequence durationInFrames ={90} >
<SceneB />
</TransitionSeries.Sequence >
</TransitionSeries >
Custom Transitions import { TransitionPresentation } from "@remotion/transitions" ;
const customSlideUp : TransitionPresentation = {
component : ({ progress, presenting, children } ) => (
<div
style ={{
position: "absolute ",
width: "100 %",
height: "100 %",
transform: presenting
? `translateY (${(1 - progress ) * 100 }%)`
: `translateY (${progress * -30 }%)`,
opacity: presenting ? 1 : 1 - progress * 0.3 ,
}}
>
{children}
</div >
),
};
<TransitionSeries.Transition
presentation ={customSlideUp}
timing ={springTiming({ config: { damping: 200 }, durationInFrames: 30 })}
/>
OffthreadVideo Use <OffthreadVideo> instead of <Video> for better rendering performance. It extracts frames as images during render (no video decoding in the browser).
import { OffthreadVideo , staticFile, AbsoluteFill } from "remotion" ;
export const VideoScene : React .FC = () => (
<AbsoluteFill >
{/* Background video */}
<OffthreadVideo
src ={staticFile( "background.mp4 ")}
style ={{ width: "100 %", height: "100 %", objectFit: "cover " }}
/>
{/* Overlay content */}
<AbsoluteFill style ={{ justifyContent: "center ", alignItems: "center " }}>
<h1 style ={{ color: "white ", fontSize: 80 , textShadow: "0 4px 20px rgba (0 ,0 ,0 ,0.5 )" }}>
Title Over Video
</h1 >
</AbsoluteFill >
</AbsoluteFill >
);
OffthreadVideo with Timing import { OffthreadVideo , Sequence } from "remotion" ;
<OffthreadVideo
src ={staticFile( "footage.mp4 ")}
startFrom ={150} // Frame 150 = 5 seconds at 30fps
/>
<OffthreadVideo
src ={staticFile( "footage.mp4 ")}
volume ={0}
/>
<OffthreadVideo
src ={staticFile( "footage.mp4 ")}
volume ={(f) => interpolate(f, [0, 30], [0, 1], { extrapolateRight: "clamp" })}
/>
Audio
Basic Audio import { Audio , staticFile, useCurrentFrame, interpolate } from "remotion" ;
export const WithAudio : React .FC = () => {
const frame = useCurrentFrame ();
const volume = interpolate (frame, [0 , 30 ], [0 , 0.8 ], {
extrapolateRight : "clamp" ,
});
return (
<>
<Audio src ={staticFile( "bgm.mp3 ")} volume ={volume} />
<Audio src ={staticFile( "sfx-whoosh.mp3 ")} volume ={0.5} startFrom ={0} />
</>
);
};
Audio with Fade Out export const FadeOutAudio : React .FC = () => {
const frame = useCurrentFrame ();
const { durationInFrames } = useVideoConfig ();
const volume = interpolate (
frame,
[0 , 30 , durationInFrames - 30 , durationInFrames],
[0 , 0.8 , 0.8 , 0 ],
{ extrapolateLeft : "clamp" , extrapolateRight : "clamp" }
);
return <Audio src ={staticFile( "music.mp3 ")} volume ={volume} /> ;
};
Audio Visualization import { getAudioData, useAudioData, visualizeAudio } from "@remotion/media-utils" ;
export const AudioVisualization : React .FC = () => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const audioData = useAudioData (staticFile ("music.mp3" ));
if (!audioData) return null ;
const visualization = visualizeAudio ({
fps,
frame,
audioData,
numberOfSamples : 256 ,
});
return (
<AbsoluteFill style ={{ alignItems: "flex-end ", justifyContent: "center " }}>
<div style ={{ display: "flex ", gap: 2 , alignItems: "flex-end ", height: 200 }}>
{visualization.map((v, i) => (
<div
key ={i}
style ={{
width: 4 ,
height: v * 200 ,
backgroundColor: `hsl (${(i / visualization.length ) * 360 }, 80 %, 60 %)`,
borderRadius: 2 ,
}}
/>
))}
</div >
</AbsoluteFill >
);
};
Rendering Configuration
Codec Comparison Codec Format Quality Speed Use Case h264MP4 Good Fast Universal playback, social media h265MP4 Better Slower Smaller files, modern devices vp8WebM Good Medium Web embedding vp9WebM Better Slow High quality web proresMOV Lossless Fast Post-production, editing gifGIF Low Fast Short loops, previews
Render Settings
npx remotion render MyVideo out/video.mp4 \
--codec=h264 \
--crf=18 \
--concurrency=4
npx remotion render MyVideo out/video.mov \
--codec=prores \
--prores-profile=4444
npx remotion render Shorts out/short.mp4 \
--width=1080 --height=1920
npx remotion still MyVideo out/thumb.png --frame=60
Multi-Scene Patterns
Scene Component Pattern
const Scene : React .FC <{
bg : string ;
children : React .ReactNode ;
}> = ({ bg, children } ) => (
<AbsoluteFill
style ={{
backgroundColor: bg ,
justifyContent: "center ",
alignItems: "center ",
padding: 80 ,
}}
>
{children}
</AbsoluteFill >
);
export const MyVideo : React .FC = () => (
<TransitionSeries >
<TransitionSeries.Sequence durationInFrames ={90} >
<Scene bg ="#1a1a2e" >
<FadeInTitle text ="Welcome" />
</Scene >
</TransitionSeries.Sequence >
<TransitionSeries.Transition
presentation ={fade()}
timing ={linearTiming({ durationInFrames: 20 })}
/>
<TransitionSeries.Sequence durationInFrames ={150} >
<Scene bg ="#16213e" >
<ContentSlide items ={data.items} />
</Scene >
</TransitionSeries.Sequence >
<TransitionSeries.Transition
presentation ={slide({ direction: "from-right " })}
timing ={springTiming({ config: { damping: 200 }, durationInFrames: 25 })}
/>
<TransitionSeries.Sequence durationInFrames ={90} >
<Scene bg ="#0f3460" >
<OutroWithCTA />
</Scene >
</TransitionSeries.Sequence >
</TransitionSeries >
);
Best Practices
<OffthreadVideo> over <Video> — Always prefer OffthreadVideo for rendering; use <Video> only when you need loop.
Organize scenes as components — Each scene is a self-contained React component with its own internal animation.
Use TransitionSeries for professional feel — Cuts between scenes feel amateur; crossfades and slides feel polished.
Audio fade in/out — Never start or stop audio abruptly. Always fade over 0.5-1 second.
CRF 18-23 for H.264 — Lower CRF = higher quality. 18 is visually lossless, 23 is default.
Test with npx remotion studio — Always preview in Studio before rendering. Catches timing issues early.
Common Pitfalls Pitfall Fix Audio cuts off abruptly Add fade-out with interpolate near durationInFrames Transitions don't work with Series Use TransitionSeries from @remotion/transitions Video freezes during render Use <OffthreadVideo> instead of <Video> Overlapping scenes visible Check Sequence from + durationInFrames don't overlap unintentionally Huge file size Lower CRF, use H.265, or reduce resolution Audio out of sync Match audio file sample rate to composition fps timing
From remotion-content
Remotion content scripting — data-driven videos, dynamic templates, captions/subtitles, calculateMetadata, parameterized rendering, and batch video generation
Remotion Content Scripting
Purpose Build data-driven video templates with Remotion. Covers parameterized rendering, calculateMetadata for dynamic props, caption/subtitle systems (SRT, TikTok-style), async data fetching with delayRender/continueRender, batch rendering from datasets, and reusable video template patterns.
Data-Driven Videos
calculateMetadata (Recommended) Fetch data and compute dynamic metadata before rendering starts:
import { z } from "zod" ;
import { CalculateMetadataFunction } from "remotion" ;
export const videoSchema = z.object ({
productId : z.string (),
product : z.nullable (z.object ({
name : z.string (),
price : z.number (),
images : z.array (z.string ()),
description : z.string (),
})),
});
type Props = z.infer <typeof videoSchema>;
export const calcMetadata : CalculateMetadataFunction <Props > = async ({
props,
}) => {
const res = await fetch (`https://api.store.com/products/${props.productId} ` );
const product = await res.json ();
const durationInFrames = (product.images .length * 3 + 4 ) * 30 ;
return {
props : { ...props, product },
durationInFrames,
};
};
<Composition
id="ProductVideo"
component={ProductVideo }
schema={videoSchema}
calculateMetadata={calcMetadata}
durationInFrames={300 }
fps={30 }
width={1080 }
height={1080 }
defaultProps={{ productId : "abc123" , product : null }}
/>
delayRender / continueRender (In-Component Async) For async operations inside the component itself:
import { useState, useEffect, useCallback } from "react" ;
import { AbsoluteFill , useDelayRender } from "remotion" ;
export const AsyncComponent : React .FC <{ apiUrl : string }> = ({ apiUrl } ) => {
const [data, setData] = useState<any >(null );
const { delayRender, continueRender, cancelRender } = useDelayRender ();
const [handle] = useState (() => delayRender ("Fetching data..." ));
const fetchData = useCallback (async () => {
try {
const response = await fetch (apiUrl);
const json = await response.json ();
setData (json);
continueRender (handle);
} catch (err) {
cancelRender (err);
}
}, [apiUrl, continueRender, cancelRender, handle]);
useEffect (() => {
fetchData ();
}, [fetchData]);
if (!data) return null ;
return (
<AbsoluteFill >
<h1 > {data.title}</h1 >
</AbsoluteFill >
);
};
Prefetching Assets import { prefetch } from "remotion" ;
const { free, waitUntilDone } = prefetch ("https://cdn.example.com/video.mp4" , {
method : "blob-url" ,
});
await waitUntilDone ();
free ();
Captions & Subtitles
Parse SRT Files import { parseSrt } from "@remotion/captions" ;
import type { Caption } from "@remotion/captions" ;
const response = await fetch (staticFile ("subtitles.srt" ));
const text = await response.text ();
const { captions } = parseSrt ({ input : text });
Simple Caption Display import { useCurrentFrame, useVideoConfig, AbsoluteFill } from "remotion" ;
import type { Caption } from "@remotion/captions" ;
const CaptionOverlay : React .FC <{ captions : Caption [] }> = ({ captions } ) => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const currentTimeMs = (frame / fps) * 1000 ;
const activeCaption = captions.find (
(c ) => c.startMs <= currentTimeMs && c.endMs > currentTimeMs
);
if (!activeCaption) return null ;
return (
<AbsoluteFill style ={{ justifyContent: "flex-end ", padding: 40 }}>
<div
style ={{
backgroundColor: "rgba (0 , 0 , 0 , 0.75 )",
color: "white ",
fontSize: 48 ,
fontWeight: 700 ,
padding: "12px 24px ",
borderRadius: 12 ,
textAlign: "center ",
maxWidth: "80 %",
alignSelf: "center ",
}}
>
{activeCaption.text}
</div >
</AbsoluteFill >
);
};
TikTok-Style Captions (Word Highlighting) import { createTikTokStyleCaptions } from "@remotion/captions" ;
import type { Caption , TikTokPage } from "@remotion/captions" ;
import { Sequence , useCurrentFrame, useVideoConfig, AbsoluteFill } from "remotion" ;
const SWITCH_EVERY_MS = 1200 ;
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 ", textAlign: "center " }}>
{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 >
);
};
export const TikTokCaptions : React .FC <{ captions : Caption [] }> = ({
captions,
} ) => {
const { fps } = useVideoConfig ();
const { pages } = createTikTokStyleCaptions ({
captions,
combineTokensWithinMilliseconds : SWITCH_EVERY_MS ,
});
return (
<AbsoluteFill >
{pages.map((page, i) => {
const nextPage = pages[i + 1] ?? null;
const startFrame = (page.startMs / 1000) * fps;
const endFrame = nextPage
? (nextPage.startMs / 1000) * fps
: startFrame + (SWITCH_EVERY_MS / 1000) * fps;
const durationInFrames = Math.max(1, Math.round(endFrame - startFrame));
return (
<Sequence
key ={i}
from ={Math.round(startFrame)}
durationInFrames ={durationInFrames}
>
<CaptionPage page ={page} />
</Sequence >
);
})}
</AbsoluteFill >
);
};
Whisper Integration (AI Transcription) npm i @remotion/install-whisper-cpp @remotion/whisper
import { installWhisperCpp } from "@remotion/install-whisper-cpp" ;
import { transcribe } from "@remotion/whisper" ;
await installWhisperCpp ({ version : "1.5.5" });
const result = await transcribe ({
inputPath : "public/audio.mp3" ,
whisperPath : ".whisper" ,
model : "medium" ,
tokenLevelTimestamps : true ,
});
Parameterized Rendering
Props via CLI
npx remotion render ProductVideo out/video.mp4 \
--props='{"productId":"abc123","product":null}'
npx remotion render ProductVideo out/video.mp4 \
--props=./props.json
Props via API import { renderMedia, selectComposition } from "@remotion/renderer" ;
const composition = await selectComposition ({
serveUrl : bundled,
id : "ProductVideo" ,
inputProps : {
productId : "abc123" ,
product : null ,
},
});
await renderMedia ({
composition,
serveUrl : bundled,
codec : "h264" ,
outputLocation : "out/product-abc123.mp4" ,
});
Batch Rendering from Dataset
Render Multiple Videos
import { bundle } from "@remotion/bundler" ;
import { renderMedia, selectComposition } from "@remotion/renderer" ;
const products = [
{ id : "abc123" , name : "Widget A" },
{ id : "def456" , name : "Widget B" },
{ id : "ghi789" , name : "Widget C" },
];
async function renderAll ( ) {
const bundled = await bundle ({ entryPoint : "./src/index.ts" });
for (const product of products) {
console .log (`Rendering ${product.name} ...` );
const composition = await selectComposition ({
serveUrl : bundled,
id : "ProductVideo" ,
inputProps : { productId : product.id , product : null },
});
await renderMedia ({
composition,
serveUrl : bundled,
codec : "h264" ,
outputLocation : `out/${product.id} .mp4` ,
});
console .log (`Done: ${product.name} ` );
}
}
renderAll ();
Parallel Batch Rendering import pLimit from "p-limit" ;
const limit = pLimit (3 );
await Promise .all (
products.map ((product ) =>
limit (async () => {
const composition = await selectComposition ({
serveUrl : bundled,
id : "ProductVideo" ,
inputProps : { productId : product.id , product : null },
});
await renderMedia ({
composition,
serveUrl : bundled,
codec : "h264" ,
outputLocation : `out/${product.id} .mp4` ,
concurrency : 2 ,
});
})
)
);
Template Patterns
Reusable Video Template
import { z } from "zod" ;
export const socialPostSchema = z.object ({
headline : z.string (),
body : z.string (),
imageUrl : z.string ().url (),
brandColor : z.string ().default ("#3b82f6" ),
ctaText : z.string ().default ("Learn More" ),
format : z.enum (["square" , "story" , "landscape" ]).default ("square" ),
});
type Props = z.infer <typeof socialPostSchema>;
const FORMATS = {
square : { width : 1080 , height : 1080 },
story : { width : 1080 , height : 1920 },
landscape : { width : 1920 , height : 1080 },
};
export const calcSocialPostMetadata : CalculateMetadataFunction <Props > = async ({
props,
}) => ({
...FORMATS [props.format ],
durationInFrames : 150 ,
});
export const SocialPost : React .FC <Props > = ({
headline,
body,
imageUrl,
brandColor,
ctaText,
} ) => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const headlineOpacity = interpolate (frame, [0 , 20 ], [0 , 1 ], {
extrapolateRight : "clamp" ,
});
const headlineY = spring ({ frame, fps, config : { damping : 15 } });
return (
<AbsoluteFill style ={{ backgroundColor: brandColor }}>
<Img src ={imageUrl} style ={{ width: "100 %", height: "60 %", objectFit: "cover " }} />
<div style ={{ padding: 40 , color: "white " }}>
<h1
style ={{
fontSize: 56 ,
opacity: headlineOpacity ,
transform: `translateY (${interpolate (headlineY , [0 , 1 ], [30 , 0 ])}px )`,
}}
>
{headline}
</h1 >
<p style ={{ fontSize: 28 , marginTop: 16 , opacity: headlineOpacity }}> {body}</p >
</div >
</AbsoluteFill >
);
};
Best Practices
calculateMetadata over delayRender — Prefer calculateMetadata for data fetching; it runs once before render and can set dynamic duration/resolution.
Zod schemas for all templates — Enables Remotion Studio prop editor UI and type safety.
Separate data from presentation — Keep fetching logic in calculateMetadata, rendering logic in components.
Batch with concurrency limits — Don't render too many videos in parallel; 2-4 concurrent renders is optimal.
Cache API responses — If rendering multiple videos from the same API, cache responses to avoid rate limits.
TikTok captions: 1-2 second grouping — combineTokensWithinMilliseconds: 1200 reads naturally.
Test templates with edge cases — Long text, missing images, empty arrays. Templates must handle all valid inputs.
Common Pitfalls Pitfall Fix calculateMetadata data not serializableAll data must be JSON-serializable (no Date objects, functions) delayRender timeoutDefault 30s timeout; increase with delayRender("msg", { timeoutInMilliseconds: 60000 }) Captions out of sync Verify SRT timestamps match actual audio timing Batch render OOM Limit concurrency with p-limit, reduce per-render concurrency Template breaks on edge case Validate props with Zod, add fallback defaults continueRender never calledAlways wrap in try-catch; use cancelRender on error
From remotion-animation
Remotion animation patterns — spring physics, interpolate, Easing curves, timing, stagger, enter/exit, and complex motion sequences
Remotion Animation Patterns
Purpose Master animation in Remotion using interpolate, spring, Easing, and interpolateColors. Covers timing patterns, staggered entrances, text animations, complex motion sequences, and performance-optimized animation techniques for programmatic video.
Core: interpolate Map frame numbers to any value range:
import { useCurrentFrame, interpolate, AbsoluteFill } from "remotion" ;
export const BasicAnimation : React .FC = () => {
const frame = useCurrentFrame ();
const opacity = interpolate (frame, [0 , 30 ], [0 , 1 ], {
extrapolateRight : "clamp" ,
});
const translateY = interpolate (frame, [0 , 30 ], [40 , 0 ], {
extrapolateRight : "clamp" ,
});
const scale = interpolate (frame, [0 , 30 ], [0.8 , 1 ], {
extrapolateLeft : "clamp" ,
extrapolateRight : "clamp" ,
});
const rotation = interpolate (frame, [0 , 60 ], [0 , 360 ]);
return (
<AbsoluteFill style ={{ justifyContent: "center ", alignItems: "center " }}>
<div
style ={{
opacity ,
transform: `translateY (${translateY }px ) scale (${scale }) rotate (${rotation }deg )`,
}}
>
Hello
</div >
</AbsoluteFill >
);
};
Multi-Segment Interpolation
const opacity = interpolate (
frame,
[0 , 20 , 80 , 100 ],
[0 , 1 , 1 , 0 ],
{ extrapolateLeft : "clamp" , extrapolateRight : "clamp" }
);
const scale = interpolate (
frame,
[0 , 15 , 25 , 35 ],
[0 , 1.2 , 0.95 , 1 ],
{ extrapolateRight : "clamp" }
);
interpolateColors import { interpolateColors, useCurrentFrame } from "remotion" ;
const frame = useCurrentFrame ();
const color = interpolateColors (
frame,
[0 , 30 , 60 ],
["#3b82f6" , "#8b5cf6" , "#ec4899" ]
);
<div style ={{ backgroundColor: color }}> Gradient over time</div >
Core: spring Physics-based animation with natural feel:
import { spring, useCurrentFrame, useVideoConfig } from "remotion" ;
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const progress = spring ({ frame, fps });
const bouncy = spring ({
frame,
fps,
config : {
damping : 8 ,
stiffness : 200 ,
mass : 0.5 ,
},
});
const scale = spring ({
frame,
fps,
from : 0.5 ,
to : 1 ,
config : { damping : 12 , stiffness : 200 },
});
const delayed = spring ({
frame : frame - 30 ,
fps,
config : { damping : 15 },
});
Spring Configurations Feel damping stiffness mass Use Case Snappy 20 300 0.5 UI elements, quick pops Bouncy 8 200 1 Playful, attention-grabbing Smooth 15 100 1 Gentle entrances Stiff 30 400 0.5 Sharp, decisive motion Heavy 12 80 2 Weighty, impactful Elastic 5 150 0.8 Rubber-band, fun
Combining spring + interpolate Map spring progress to custom ranges:
const springProgress = spring ({
frame,
fps,
config : { damping : 12 , stiffness : 200 },
});
const rotation = interpolate (springProgress, [0 , 1 ], [0 , 360 ]);
const translateX = interpolate (springProgress, [0 , 1 ], [-200 , 0 ]);
const scale = interpolate (springProgress, [0 , 0.5 , 1 ], [0 , 1.15 , 1 ]);
Easing Functions Custom easing curves for non-spring interpolations:
import { Easing , interpolate, useCurrentFrame } from "remotion" ;
const frame = useCurrentFrame ();
const easeIn = interpolate (frame, [0 , 30 ], [0 , 1 ], {
easing : Easing .in (Easing .ease ),
extrapolateRight : "clamp" ,
});
const easeOut = interpolate (frame, [0 , 30 ], [0 , 1 ], {
easing : Easing .out (Easing .ease ),
extrapolateRight : "clamp" ,
});
const easeInOut = interpolate (frame, [0 , 30 ], [0 , 1 ], {
easing : Easing .inOut (Easing .ease ),
extrapolateRight : "clamp" ,
});
const custom = interpolate (frame, [0 , 30 ], [0 , 1 ], {
easing : Easing .bezier (0.4 , 0 , 0.2 , 1 ),
extrapolateRight : "clamp" ,
});
const elastic = interpolate (frame, [0 , 60 ], [0 , 1 ], {
easing : Easing .elastic (2 ),
extrapolateRight : "clamp" ,
});
const bounce = interpolate (frame, [0 , 60 ], [0 , 1 ], {
easing : Easing .bounce ,
extrapolateRight : "clamp" ,
});
Easing Reference Easing Effect Use Case Easing.linearConstant speed Progress bars only Easing.easeSmooth acceleration/deceleration General purpose Easing.in(Easing.ease)Slow start, fast end Elements leaving Easing.out(Easing.ease)Fast start, slow end Elements entering Easing.inOut(Easing.ease)Smooth both ends Elements moving Easing.bezier(...)Custom curve Precise control Easing.elastic(n)Spring-like overshoot Playful entrances Easing.bounceBouncing ball Fun, attention-grabbing Easing.circleCircular curve Subtle acceleration Easing.expExponential Dramatic speed changes
Staggered Animations
Stagger Array Items const items = ["Design" , "Develop" , "Deploy" ];
const STAGGER_DELAY = 8 ;
export const StaggeredList : React .FC = () => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
return (
<AbsoluteFill style ={{ justifyContent: "center ", padding: 80 }}>
{items.map((item, index) => {
const delay = index * STAGGER_DELAY;
const opacity = interpolate(frame - delay, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const translateX = spring({
frame: frame - delay,
fps,
config: { damping: 15, stiffness: 200 },
});
return (
<div
key ={item}
style ={{
fontSize: 64 ,
fontWeight: 700 ,
opacity ,
transform: `translateX (${interpolate (translateX , [0 , 1 ], [-60 , 0 ])}px )`,
marginBottom: 20 ,
}}
>
{item}
</div >
);
})}
</AbsoluteFill >
);
};
Stagger with Exit const ENTER_DELAY = 8 ;
const EXIT_START = 80 ;
const EXIT_DELAY = 5 ;
items.map ((item, index ) => {
const enterOffset = index * ENTER_DELAY ;
const enterOpacity = interpolate (frame - enterOffset, [0 , 15 ], [0 , 1 ], {
extrapolateLeft : "clamp" ,
extrapolateRight : "clamp" ,
});
const exitOffset = (items.length - 1 - index) * EXIT_DELAY ;
const exitFrame = EXIT_START + exitOffset;
const exitOpacity = interpolate (frame, [exitFrame, exitFrame + 15 ], [1 , 0 ], {
extrapolateLeft : "clamp" ,
extrapolateRight : "clamp" ,
});
const opacity = Math .min (enterOpacity, exitOpacity);
});
Text Animations
Character-by-Character Reveal export const TypewriterText : React .FC <{ text : string }> = ({ text } ) => {
const frame = useCurrentFrame ();
const charsToShow = Math .floor (
interpolate (frame, [0 , text.length * 2 ], [0 , text.length ], {
extrapolateRight : "clamp" ,
})
);
return (
<div style ={{ fontSize: 48 , fontFamily: "monospace " }}>
{text.slice(0, charsToShow)}
{charsToShow < text.length && (
<span style ={{ opacity: frame % 15 < 8 ? 1 : 0 }}> |</span >
)}
</div >
);
};
Word-by-Word Spring Entrance export const WordSpring : React .FC <{ text : string }> = ({ text } ) => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const words = text.split (" " );
return (
<div style ={{ display: "flex ", flexWrap: "wrap ", gap: 12 , fontSize: 64 , fontWeight: 700 }}>
{words.map((word, i) => {
const delay = i * 5;
const progress = spring({
frame: frame - delay,
fps,
config: { damping: 12, stiffness: 200 },
});
return (
<span
key ={i}
style ={{
opacity: interpolate (progress , [0 , 0.5 ], [0 , 1 ], { extrapolateRight: "clamp " }),
transform: `translateY (${interpolate (progress , [0 , 1 ], [30 , 0 ])}px )`,
display: "inline-block ",
}}
>
{word}
</span >
);
})}
</div >
);
};
Counter Animation export const Counter : React .FC <{ from : number ; to : number }> = ({ from , to } ) => {
const frame = useCurrentFrame ();
const value = interpolate (frame, [0 , 60 ], [from , to], {
extrapolateRight : "clamp" ,
easing : Easing .out (Easing .ease ),
});
return (
<div style ={{ fontSize: 120 , fontWeight: 800 , fontVariantNumeric: "tabular-nums " }}>
{Math.round(value).toLocaleString()}
</div >
);
};
Complex Motion Sequences
Chained Animations (Sequential) export const ChainedMotion : React .FC = () => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const logoScale = spring ({
frame,
fps,
config : { damping : 12 , stiffness : 200 },
});
const titleProgress = spring ({
frame : frame - 20 ,
fps,
config : { damping : 15 , stiffness : 150 },
});
const titleX = interpolate (titleProgress, [0 , 1 ], [-100 , 0 ]);
const titleOpacity = interpolate (titleProgress, [0 , 0.3 ], [0 , 1 ], {
extrapolateRight : "clamp" ,
});
const subtitleOpacity = interpolate (frame, [40 , 60 ], [0 , 1 ], {
extrapolateLeft : "clamp" ,
extrapolateRight : "clamp" ,
});
const ctaScale = spring ({
frame : frame - 55 ,
fps,
config : { damping : 8 , stiffness : 200 },
});
return (
<AbsoluteFill style ={{ justifyContent: "center ", alignItems: "center ", gap: 20 }}>
<div style ={{ transform: `scale (${logoScale })` }}> Logo</div >
<h1 style ={{ transform: `translateX (${titleX }px )`, opacity: titleOpacity }}>
Title
</h1 >
<p style ={{ opacity: subtitleOpacity }}> Subtitle text here</p >
<button style ={{ transform: `scale (${ctaScale })` }}> Call to Action</button >
</AbsoluteFill >
);
};
Looping Animation
const rotation = interpolate (frame % 60 , [0 , 60 ], [0 , 360 ]);
const pulse = interpolate (
Math .sin ((frame / 30 ) * Math .PI ),
[-1 , 1 ],
[0.95 , 1.05 ]
);
const float = Math .sin ((frame / fps) * Math .PI * 0.5 ) * 10 ;
Path Animation
const angle = interpolate (frame, [0 , 120 ], [0 , Math .PI * 2 ]);
const radius = 150 ;
const x = Math .cos (angle) * radius;
const y = Math .sin (angle) * radius;
<div style ={{ transform: `translate (${x }px , ${y }px )` }}>
Moving in a circle
</div >
const t = interpolate (frame, [0 , 60 ], [0 , 1 ], { extrapolateRight : "clamp" });
const bezierX = interpolate (t, [0 , 0.5 , 1 ], [0 , 200 , 400 ]);
const bezierY = interpolate (t, [0 , 0.5 , 1 ], [0 , -100 , 0 ]);
Noise & Organic Motion import { random } from "remotion" ;
const wobbleX = random (`wobble-x-${frame} ` ) * 4 - 2 ;
const wobbleY = random (`wobble-y-${frame} ` ) * 4 - 2 ;
const smoothNoise = (
Math .sin (frame * 0.1 ) * 3 +
Math .sin (frame * 0.23 ) * 2 +
Math .sin (frame * 0.37 ) * 1
);
const shakeX = Math .sin (frame * 0.5 ) * 2 + Math .cos (frame * 0.7 ) * 1.5 ;
const shakeY = Math .cos (frame * 0.4 ) * 2 + Math .sin (frame * 0.9 ) * 1 ;
const shakeRotation = Math .sin (frame * 0.3 ) * 0.5 ;
Best Practices
Always clamp extrapolation — Use extrapolateRight: "clamp" to prevent values overshooting beyond the target range.
Spring for organic, interpolate for precise — Springs feel natural; interpolate gives exact timing control.
Stagger at 5-10 frame intervals — Too fast looks simultaneous, too slow loses cohesion.
Chain with frame offsets — Start phase 2 before phase 1 ends for overlapping, fluid sequences.
Use random(seed) not Math.random() — Deterministic rendering requires deterministic randomness.
Test at 1x speed — Don't judge animation timing in slow-motion preview.
Reuse spring configs — Define configs as constants for consistent animation language across the video.
Common Pitfalls Pitfall Fix Missing extrapolateRight: "clamp" Values go beyond intended range (opacity > 1, negative positions) Math.random() in renderDifferent frame = different random value = flickering. Use random(seed) Spring with negative frame Returns from value — this is correct behavior, not a bug Over-complicated interpolation Break complex animations into multiple simple interpolations No easing on interpolate Linear motion looks robotic — add easing: Easing.out(Easing.ease) Stagger delay too small Items animate simultaneously — use at least 5 frames between items
From motion-design
Motion design for web — Framer Motion patterns, Remotion video composition, 12 Disney animation principles applied to UI, easing curves, and accessible motion
Motion Design
Purpose Expert guidance on motion design for web applications: Framer Motion for UI animation, Remotion for programmatic video, the 12 Disney animation principles mapped to digital interfaces, easing curve selection, and accessible motion that respects prefers-reduced-motion.
Framer Motion
Core API Patterns
Basic Animation import { motion } from "framer-motion" ;
<motion.div
initial ={{ opacity: 0 , y: 20 }}
animate ={{ opacity: 1 , y: 0 }}
transition ={{ duration: 0.3 , ease: "easeOut " }}
/>
<motion.button
whileHover ={{ scale: 1.02 }}
whileTap ={{ scale: 0.98 }}
transition ={{ type: "spring ", stiffness: 400 , damping: 25 }}
/>
Variants (Declarative Animation States) const cardVariants = {
hidden : { opacity : 0 , y : 20 },
visible : { opacity : 1 , y : 0 },
exit : { opacity : 0 , y : -10 },
};
<motion.div
variants ={cardVariants}
initial ="hidden"
animate ="visible"
exit ="exit"
transition ={{ duration: 0.3 }}
/>
Stagger Children const containerVariants = {
hidden : {},
visible : {
transition : {
staggerChildren : 0.08 ,
delayChildren : 0.1 ,
},
},
};
const itemVariants = {
hidden : { opacity : 0 , y : 12 },
visible : { opacity : 1 , y : 0 },
};
<motion.ul variants ={containerVariants} initial ="hidden" animate ="visible" >
{items.map((item) => (
<motion.li key ={item.id} variants ={itemVariants} />
))}
</motion.ul >
AnimatePresence (Enter/Exit Animations) import { AnimatePresence , motion } from "framer-motion" ;
<AnimatePresence mode ="wait" >
{isVisible && (
<motion.div
key ="content"
initial ={{ opacity: 0 , height: 0 }}
animate ={{ opacity: 1 , height: "auto " }}
exit ={{ opacity: 0 , height: 0 }}
transition ={{ duration: 0.2 }}
/>
)}
</AnimatePresence >
"sync" (default) — Enter and exit animate simultaneously
"wait" — Exit completes before enter starts
"popLayout" — Exiting elements removed from layout flow immediately
Layout Animations
<motion.div layout />
<motion.div layoutId ="shared-element" />
<motion.div
layout
transition ={{ layout: { duration: 0.3 , ease: "easeInOut " } }}
/>
Scroll Animations import { motion, useScroll, useTransform } from "framer-motion" ;
function ParallaxHero ( ) {
const { scrollYProgress } = useScroll ();
const y = useTransform (scrollYProgress, [0 , 1 ], [0 , -200 ]);
const opacity = useTransform (scrollYProgress, [0 , 0.5 ], [1 , 0 ]);
return (
<motion.div style ={{ y , opacity }}>
Hero content
</motion.div >
);
}
Scroll-linked with element ref:
function ScrollReveal ({ children }: { children: ReactNode } ) {
const ref = useRef (null );
const { scrollYProgress } = useScroll ({
target : ref,
offset : ["start end" , "end start" ],
});
const opacity = useTransform (scrollYProgress, [0 , 0.3 ], [0 , 1 ]);
const y = useTransform (scrollYProgress, [0 , 0.3 ], [40 , 0 ]);
return (
<motion.div ref ={ref} style ={{ opacity , y }}>
{children}
</motion.div >
);
}
Gesture Handlers <motion.div
drag
drag="x"
dragConstraints={{ left : -100 , right : 100 }}
dragElastic={0.2 }
dragTransition={{ bounceStiffness : 300 , bounceDamping : 20 }}
onDragEnd={(event, info ) => {
if (info.offset .x > 100 ) handleSwipeRight ();
if (info.offset .x < -100 ) handleSwipeLeft ();
}}
whileDrag={{ scale : 1.05 , cursor : "grabbing" }}
/>
Shared Layout (Cross-Component) import { LayoutGroup , motion } from "framer-motion" ;
<LayoutGroup >
{tabs.map((tab) => (
<button key ={tab.id} onClick ={() => setActive(tab.id)}>
{tab.label}
{active === tab.id && (
<motion.div
layoutId ="active-tab-indicator"
className ="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600"
/>
)}
</button >
))}
</LayoutGroup >
Framer Motion Performance Tips 1. Use `transform` and `opacity` only — these are GPU-accelerated
2. Avoid animating `width`, `height`, `padding`, `margin` directly
3. Use `layout` prop for size/position changes instead
4. Set `layoutScroll` on scrollable containers to fix layout calc
5. Use `will-change: transform` sparingly (Framer adds it automatically)
6. Prefer `spring` over `tween` — springs feel more natural and don't need duration
7. Memoize variants objects outside components to prevent re-renders
8. Use `useReducedMotion()` hook to conditionally disable animations
Remotion
Core Concepts Remotion renders React components as video frames. Each frame is a render at a specific frame number.
Composition Setup
import { Composition } from "remotion" ;
import { MyVideo } from "./MyVideo" ;
export const RemotionRoot = ( ) => (
<Composition
id ="MyVideo"
component ={MyVideo}
durationInFrames ={300} // 10 seconds at 30fps
fps ={30}
width ={1920}
height ={1080}
defaultProps ={{ title: "Hello " }}
/>
);
Using Frame and Time import { useCurrentFrame, useVideoConfig, interpolate } from "remotion" ;
export const MyVideo = ({ title }: { title: string } ) => {
const frame = useCurrentFrame ();
const { fps, durationInFrames, width, height } = useVideoConfig ();
const opacity = interpolate (
frame,
[0 , 30 ],
[0 , 1 ],
{ extrapolateRight : "clamp" }
);
const translateY = interpolate (
frame,
[0 , 30 ],
[40 , 0 ],
{ extrapolateRight : "clamp" }
);
return (
<div style ={{ opacity , transform: `translateY (${translateY }px )` }}>
<h1 > {title}</h1 >
</div >
);
};
Sequences (Timeline Segments) import { Sequence } from "remotion" ;
export const Timeline = ( ) => (
<>
<Sequence from ={0} durationInFrames ={90} >
<IntroScene />
</Sequence >
<Sequence from ={90} durationInFrames ={120} >
<MainContent />
</Sequence >
<Sequence from ={210} durationInFrames ={90} >
<OutroScene />
</Sequence >
</>
);
Audio import { Audio , staticFile, interpolate, useCurrentFrame } from "remotion" ;
export const WithAudio = ( ) => {
const frame = useCurrentFrame ();
const volume = interpolate (frame, [0 , 30 ], [0 , 1 ], { extrapolateRight : "clamp" });
return (
<>
<Audio src ={staticFile( "bgm.mp3 ")} volume ={volume} />
<VideoContent />
</>
);
};
Spring Animations in Remotion import { spring, useCurrentFrame, useVideoConfig } from "remotion" ;
export const SpringAnimation = ( ) => {
const frame = useCurrentFrame ();
const { fps } = useVideoConfig ();
const scale = spring ({
frame,
fps,
config : { damping : 12 , stiffness : 200 , mass : 0.5 },
});
return <div style ={{ transform: `scale (${scale })` }}> Bouncy!</div > ;
};
Rendering Pipeline
npx remotion studio
npx remotion render MyVideo out/video.mp4
npx remotion render MyVideo out/video.mp4 --frames=0-90
npx remotion render MyVideo out/video.gif --codec=gif
npx remotion render MyVideo out/video.mp4 --width=1080 --height=1080
npx remotion lambda render MyVideo
Remotion Best Practices 1. Keep compositions pure — no side effects, no async in render
2. Use `staticFile()` for assets in the `public/` folder
3. Use `<Img>` component instead of `<img>` for preloading
4. Prefetch large assets with `prefetch()` or `delayRender()` / `continueRender()`
5. Use `interpolate()` with `extrapolateRight: "clamp"` to prevent value overflow
6. Keep fps consistent (30 for social media, 60 for presentations)
7. Use `<Series>` for sequential, non-overlapping scenes (simpler than manual offsets)
12 Disney Animation Principles (Web-Mapped) The 12 principles of animation, mapped to practical web interactions:
1. Squash and Stretch Conveys weight and flexibility. In web: scale transforms on interaction.
whileTap={{ scaleX : 1.05 , scaleY : 0.95 }}
whileHover={{ scaleY : 1.02 }}
2. Anticipation Prepare the user for an action. In web: brief reverse movement before primary motion.
animate={{ y : [0 , 4 , -20 , 0 ] }}
transition={{ duration : 0.5 , times : [0 , 0.15 , 0.6 , 1 ] }}
3. Staging Direct attention to the important element. In web: dim surroundings, spotlight focal point.
<motion.div className="bg-black/50" animate={{ opacity : 1 }} />
<motion.div animate ={{ scale: 1 }} initial ={{ scale: 0.95 }} />
4. Straight Ahead vs. Pose to Pose In web: keyframe animations (pose-to-pose) vs. physics-based springs (straight ahead).
animate={{ x : [0 , 100 , 200 ] }}
transition={{ duration : 0.6 , times : [0 , 0.4 , 1 ] }}
transition={{ type : "spring" , stiffness : 300 , damping : 20 }}
5. Follow-Through and Overlapping Action Elements don't stop at the same time. In web: stagger children, spring overshoot.
transition={{ type : "spring" , stiffness : 200 , damping : 15 }}
staggerChildren : 0.05
6. Slow In, Slow Out (Easing) Natural motion accelerates and decelerates. In web: ease-in-out, never linear for UI.
transition={{ ease : "easeInOut" }}
transition={{ ease : [0.4 , 0 , 0.2 , 1 ] }}
7. Arcs Natural motion follows curved paths. In web: use CSS offset-path or animate both x and y.
animate={{ x : 100 , y : [0 , -30 , 0 ] }}
8. Secondary Action Supporting animations that enhance the primary. In web: icon spin inside button, shimmer on card hover.
whileHover={{ y : -4 , boxShadow : "0 12px 24px rgba(0,0,0,0.15)" }}
9. Timing Duration defines the feel. Fast = snappy/urgent. Slow = graceful/calm.
Micro-interactions: 100-200ms (button clicks, toggles)
Transitions: 200-400ms (page transitions, modals)
Emphasis: 400-800ms (hero animations, onboarding)
Never exceed 1s for UI animations
10. Exaggeration Amplify motion for clarity. In web: slightly larger scale changes than physically realistic.
whileHover={{ y : -6 , scale : 1.02 }}
11. Solid Drawing (Consistency) Maintain consistent visual weight and style. In web: consistent animation language.
All cards animate the same way
All modals enter/exit the same way
All buttons have the same press feel
Define variants once, reuse everywhere
12. Appeal Animation should be pleasing, not annoying. In web: purposeful, restrained, delightful.
Don't animate everything — animate the moments that matter
Subtlety > spectacle for professional interfaces
Reserve bold animation for celebrations (confetti, success states)
Easing Reference
Named Easings Name Curve Use Case linearcubic-bezier(0, 0, 1, 1)Progress bars, continuous rotation only easecubic-bezier(0.25, 0.1, 0.25, 1)General purpose (CSS default) ease-incubic-bezier(0.42, 0, 1, 1)Elements leaving the screen ease-outcubic-bezier(0, 0, 0.58, 1)Elements entering the screen ease-in-outcubic-bezier(0.42, 0, 0.58, 1)Elements moving within the screen
Material Design Easings Name Curve Use Case Standard cubic-bezier(0.4, 0, 0.2, 1)General movement Deceleration cubic-bezier(0, 0, 0.2, 1)Entering elements Acceleration cubic-bezier(0.4, 0, 1, 1)Leaving elements Sharp cubic-bezier(0.4, 0, 0.6, 1)Elements that may return
Spring Configurations Feel Stiffness Damping Mass Use Case Snappy 400 25 0.5 Button press, toggle Bouncy 200 15 1 Playful interactions Smooth 100 20 1 Page transitions Gentle 50 10 1 Slow reveals Stiff 600 30 0.5 Quick snaps
Choosing Easing Entering screen → ease-out (decelerate into view)
Leaving screen → ease-in (accelerate out of view)
Moving on screen → ease-in-out (smooth transition)
Interactive feedback → spring (natural, responsive feel)
Progress/loading → linear (constant rate)
Accessibility: prefers-reduced-motion
The Requirement Users who experience motion sickness, vestibular disorders, or simply prefer less animation set prefers-reduced-motion: reduce in their OS settings. All motion must respect this preference.
CSS Approach
.card {
transition : transform 0.3s ease, box-shadow 0.3s ease;
}
.card :hover {
transform : translateY (-4px );
box-shadow : 0 12px 24px rgba (0 ,0 ,0 ,0.15 );
}
@media (prefers-reduced-motion : reduce) {
.card {
transition : none;
}
.card :hover {
transform : none;
box-shadow : 0 4px 8px rgba (0 ,0 ,0 ,0.1 );
}
}
Tailwind Approach <div class ="transition-all duration-200 hover:-translate-y-1 motion-reduce:transition-none motion-reduce:hover:translate-y-0" >
Content
</div >
Framer Motion Approach import { useReducedMotion } from "framer-motion" ;
function AnimatedCard ({ children }: { children: ReactNode } ) {
const shouldReduceMotion = useReducedMotion ();
return (
<motion.div
initial ={shouldReduceMotion ? false : { opacity: 0 , y: 20 }}
animate ={{ opacity: 1 , y: 0 }}
transition ={shouldReduceMotion ? { duration: 0 } : { duration: 0.3 }}
>
{children}
</motion.div >
);
}
Vestibular-Safe Alternatives When reducing motion, don't just remove it — provide alternatives:
Full Motion Reduced Alternative Slide in from side Fade in (opacity only) Scale up/bounce Fade in (opacity only) Parallax scrolling Static positioning Auto-playing animation User-triggered only Page transition slide Instant cut or crossfade Continuous rotation Static icon
What's Always Safe These effects don't trigger vestibular issues and can remain:
Color changes (hover state color shifts)
Opacity transitions (fades)
Border/outline changes
Box-shadow changes (without position shift)
Very short, very small transforms (< 5px, < 100ms)
Pitfalls
Animating layout properties — Never directly animate width, height, padding, margin. Use transform: scale() or Framer Motion's layout prop.
Linear easing for UI — Linear motion looks robotic. Always use easing or springs for UI interactions.
Missing exit animations — Abrupt disappearance is jarring. Use AnimatePresence for all conditional renders.
Over-animating — If everything moves, nothing stands out. Animate the moments that matter.
Ignoring prefers-reduced-motion — This is not optional. It's an accessibility requirement.
Too-long durations — UI animations > 500ms feel sluggish. Keep under 400ms for most interactions.
Remotion in production apps — Remotion is for video rendering, not runtime UI. Don't import it into your web app bundle.
Spring without damping — Underdamped springs oscillate forever. Always set a reasonable damping value (15-30 for most cases).