remotion-video
Scaffold a Remotion video project with multi-format compositions, spring animations, beat-synced audio, and reusable scene primitives. Outputs a project you can npx remotion studio immediately.
Phase 1: Get the Brief
Before writing any code, ask for what's missing:
I'll scaffold a Remotion video project. Quick decisions:
1. Product name? (used for composition IDs and file names)
2. One-line pitch? (the core message of the video)
3. Scene ideas? (3-8 scenes — what story does each beat tell?)
4. Color vibe?
a) Dark + amber accent (#d97706) — warm, editorial
b) Dark + monochrome — clean, minimal
c) Custom — give me bg, text, and accent hex values
5. BPM of your music track? (default: 115)
6. Font? (default: system stack — or provide .woff2 files for Geist/custom)
If the user already described their video in the conversation, extract answers before asking. Don't ask for what you already have.
Defaults if user skips: 25 seconds, 30fps, 6 scenes, dark + amber, 115 BPM, system font stack.
Phase 2: Scaffold the Project
Create this exact file tree:
[name]-video/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts
│ ├── Root.tsx
│ ├── [Name]Demo.tsx # orchestrator
│ └── [Name]Scenes.tsx # scenes + tokens + primitives
├── public/
│ ├── audio/ # user drops .mp3 here
│ └── fonts/ # .woff2 files if custom font
└── out/
package.json
{
"name": "[name]-video",
"type": "commonjs",
"dependencies": {
"@remotion/cli": "^4.0.421",
"@types/react": "^19.2.13",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"remotion": "^4.0.421",
"typescript": "^5.9.3"
}
}
Add @remotion/fonts: "^4.0.434" only if loading local font files. Don't include it otherwise.
"type": "commonjs" is required. ESM causes issues with Remotion's bundler.
tsconfig.json
{
"compilerOptions": {
"target": "ES2018",
"module": "commonjs",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
src/index.ts
Always identical:
import { registerRoot } from "remotion";
import { RemotionRoot } from "./Root";
registerRoot(RemotionRoot);
Phase 3: Compositions (Root.tsx)
Register three compositions. Same component renders all three — aspect ratio detection happens inside.
import { Composition } from "remotion";
import { Demo } from "./[Name]Demo";
export const RemotionRoot = () => (
<>
<Composition id="[Name]Demo" component={Demo} durationInFrames={750} fps={30} width={1920} height={1080} />
<Composition id="[Name]DemoMobile" component={Demo} durationInFrames={750} fps={30} width={1080} height={1920} />
<Composition id="[Name]DemoMobileLandscape" component={Demo} durationInFrames={750} fps={30} width={1334} height={750} />
</>
);
750 frames at 30fps = 25 seconds. Adjust if user specified a different duration.
Never build separate mobile components. Inside the component, detect format:
const { width, height, fps } = useVideoConfig();
const isPhone = height > width;
const isMobileLandscape = width > height && width <= 1400;
const globalScale = isPhone ? 1.14 : isMobileLandscape ? 1.08 : 1;
Wrap the entire scene area in a scaled <AbsoluteFill> using globalScale.
If loading custom fonts, call the font loader at module level in Root.tsx before compositions.
Phase 4: Scene Architecture (The Orchestrator)
The orchestrator file ([Name]Demo.tsx) does three things: defines timing, wires audio, and lays out sequences.
Scene Timing Object
const scenes = {
hook: { from: 0, dur: 90 },
xfade1: { from: 86, dur: 10 },
pain: { from: 90, dur: 120 },
xfade2: { from: 206, dur: 10 },
// ... continue for each scene
} as const;
Rules:
- Crossfades overlap with the previous scene by 4 frames (
from = prev scene end - 4) - Crossfade duration: 10-16 frames
- Scene durations: 60-195 frames (2-6.5 seconds). Short. No scene should overstay.
as constis required for TypeScript to treat values as literals
Sequence Layout
Repeating pattern for every scene + crossfade pair:
const frame = useCurrentFrame();
<Sequence from={scenes.hook.from} durationInFrames={scenes.hook.dur}>
<SceneHook frame={frame - scenes.hook.from} duration={scenes.hook.dur} />
</Sequence>
<Sequence from={scenes.xfade1.from} durationInFrames={scenes.xfade1.dur}>
<CrossfadeTransition frame={frame - scenes.xfade1.from} dur={scenes.xfade1.dur} />
</Sequence>
Always pass frame - scenes.X.from so each scene gets local frame starting at 0. Scenes must never reference the global frame.
Audio
<Audio src={staticFile("audio/music.mp3")} volume={(f) => getMusicVolume(f)} />
Build getMusicVolume using the 5-layer system in references/audio.md. Pass the scenes object and transition frame numbers.
Phase 5: Build the Scenes
Put all scene components, design tokens, and primitives in [Name]Scenes.tsx.
Design Tokens
Based on user's color choice:
Dark + amber accent:
export const colors = {
bg: "#000000", surface: "#080808", surfaceRaised: "#111111",
border: "#191919", borderHover: "#333333", muted: "#555555",
text: "#999999", heading: "#ffffff",
accent: "#d97706", accentDim: "rgba(217,119,6,0.19)",
accentGlow: "rgba(217,119,6,0.07)", accentBright: "rgba(217,119,6,0.35)",
};
Dark + monochrome:
export const colors = {
bg: "#09090b", surface: "#18181b", border: "#27272a",
textPrimary: "#e4e4e7", textSecondary: "#c2c2cb",
textMuted: "#ababb5", textDim: "#9595a0", accent: "#e4e4e7",
};
Include These Primitives
Copy from references/primitives.md:
- SceneWrap — every scene uses this. Handles fade-in/fade-out with non-monotonic inputRange guard.
- SlamText — word-by-word spring entrance. Used for headlines.
- CrossfadeTransition — opacity spike between scenes. Must have
zIndex: 100. - TypingEffect — terminal/input simulation with blinking cursor.
- GrainOverlay — SVG noise texture (amber vibe only).
opacity: 0.035,zIndex: 50. - StageLight + AmbientGlow — drifting radial gradients (amber vibe only).
Scene Playbook
Pick scenes from this menu based on the user's brief:
| Scene Type | Duration | Purpose |
|---|---|---|
| Hook | 2-4.5s | SlamText headline + spring subtext. Grabs attention. |
| Pain/Problem | 4-5s | Terminal typing showing the old way. "New session" flash resets. |
| Snap/Solution | 2-3s | One command typed, result appears, "done." with glow pulse. |
| Cooking/Loading | 1.5-2s | Emoji + shake + progress bar. Anticipation. |
| Results/Cards | 4-6.5s | Items fly in from different directions. Shows output quality. |
| Feature Grid | 2-3s | 2x3 or 3x2 grid, staggered spring entrances. |
| Feedback/Chat | 3-4s | Chat bubbles: user asks (slides right), AI responds (slides left). |
| Scroll/Carousel | 5-6.5s | Items cycle through center with enter/exit transitions. |
| Install | 3s | Headline + typing animation of install command + "done." |
| CTA | 3s | SlamText + pulsing button + URL. fadeOut: 0 (no fade, video ends). |
Common patterns inside scenes:
- Every text element uses
spring()for entrance, never rawinterpolate()for motion - Staggered entrances: delay each item by 3-6 frames
- Glow pulse on completion:
interpolate(Math.sin(frame * 0.1), [-1, 1], [0.03, 0.12]) - Subtle zoom drift:
interpolate(frame, [0, duration], [1, 1.03])
Responsive Sizing
Every size constant branches on isPhone:
const fontSize = isPhone ? 148 : 88; // headlines
const subSize = isPhone ? 72 : 46; // subtext
const cmdSize = isPhone ? 72 : 48; // terminal commands
const panelW = isPhone ? 920 : 800; // UI panels
const borderRadius = isPhone ? 20 : 14; // card corners
Phone sizes are roughly 1.4-2x desktop. Not a linear scale.
See references/cheatsheet.md for the full spring config table, responsive values, and render commands.
Phase 6: Gotchas
These will silently break your video if ignored:
- Always clamp extrapolation. Every
interpolate()call needs{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }. Without it, values explode past 0/1. - Non-monotonic inputRange crash. If
fadeOut=0in SceneWrap, the inputRange gets duplicate values. ThesafeEnd = Math.max(duration - fadeOut, fadeIn + 1)guard in the primitive fixes this. - Font timeout on render.
delayRender()for font loading can timeout on frames >700. Use--timeout=60000. - zIndex on crossfades. Crossfade overlays must have
zIndex: 100to sit above scene content. - Local frame vs global frame. Always pass
frame - scenes.X.fromto scene components. as conston scenes object. Required for TypeScript to treat values as literals.- No remotion.config.ts needed. CLI defaults work fine.
type: "commonjs"in package.json. Required. ESM breaks Remotion's bundler.- Background color on SceneWrap. Set
backgroundColor: colors.bgon the AbsoluteFill. Transparent backgrounds reveal previous scenes during crossfades. - Cursor blink rate.
Math.sin(frame * 0.25)with asymmetric thresholds[-1, -0.2, 0.2, 1] → [0, 0, 1, 1]gives realistic 60% visibility.
Verify
[ ] npm install completes without errors
[ ] npx remotion studio opens and shows all 3 compositions
[ ] Desktop composition (1920x1080) renders without crashes
[ ] Mobile portrait composition (1080x1920) renders — text is readable
[ ] Mobile landscape composition (1334x750) renders
[ ] No interpolate() calls without extrapolation clamping
[ ] SceneWrap has the safeEnd/safeDur guard for fadeOut=0
[ ] Every scene component receives local frame (frame - scenes.X.from)
[ ] Crossfade overlays have zIndex: 100
[ ] Audio plays with volume changes across transitions
[ ] Full render completes: npx remotion render [Id] out/video.mp4 --timeout=60000
More from tushaarmehtaa/tushar-skills
ship-credits
Scaffold a complete credits/token metering system for any app — database schema, backend middleware, payment webhooks, frontend state, and UI components. Goes from zero to "users can buy and spend credits" in one session.
17deploy-check
Pre-flight check before pushing to production. Catches TypeScript errors, accidentally staged secrets, pending migrations, and hygiene gaps before they hit live users.
6ship-email
Scaffold transactional and campaign email infrastructure end-to-end — provider setup, templates, user segmentation, and admin send UI. Use when the user wants to add email to their app — welcome emails, notifications, re-engagement, or bulk campaigns. Triggers on requests like "add email", "set up Resend", "email campaigns", "transactional email", "send emails to users", "welcome email", "notification emails", or any mention of email sending in an app context.
6make-skill
Turn any workflow into a properly structured Claude Code skill — YAML frontmatter, phase-based instructions, real code blocks, and a verify checklist. Use when the user wants to package a repeated workflow, create a new skill, turn a process into a slash command, or publish to the skills directory. Triggers on requests like "make a skill", "create a skill", "turn this into a skill", "new skill for...", "package this as a skill", "build a skill", "I want to publish a skill", "help me write a skill", or any request to create a reusable Claude Code skill.
6mvp-spec
Turn a rough product idea into a structured MVP spec — problem statement, personas, core loop, feature split, data model, API routes, page list, and tech stack recommendation. Write this before touching any code. Triggers on requests like "spec this out", "MVP spec", "plan this product", "what should I build first", "scope this idea", "PRD", "product spec", "write a spec for...", "help me plan this", "what do I build in v1", "product requirements", or any request to structure a product idea before writing code.
6og-image
Set up dynamic Open Graph image generation and all required meta tags so links look professional when shared on Twitter/X, LinkedIn, Slack, or anywhere that renders link previews. Triggers on requests like "OG image", "open graph", "social preview", "link preview", "Twitter card", "meta tags for sharing", "my links look broken when I share them", or any mention of how links appear when shared on social media.
6