Remotion Visual Production Skill
Built from real production experience creating the SureCart 4.2 launch video. Author: Chandresh Vaghanani
Before You Start — 2 Questions (Non-Negotiable)
Ask these 2 questions before writing any code. Keep it casual — one message, two questions. Do not skip them even if the user says "just go" or "start now." These 10 seconds prevent hours of rework.
Before I start — two quick things:
1. Light or dark background? (or share your website/screenshot and I'll pull it from there)
2. Do you have a product screenshot or mockup? (totally optional — I can build without one)
That's it. No more questions unless the user volunteers more.
What to do with the answers:
- If they share a mockup/screenshot — extract background color, accent color, font style, UI copy. Use it.
- If they share a website URL — fetch it, extract the same. Do not guess.
- If they answer light/dark only — proceed with the No-Info Fallback Defaults below.
- If they skip both questions — proceed with the No-Info Fallback Defaults below. Do not ask again.
Other assets (logo, music, extra colors) — accept if offered, but never block on them.
No-Info Fallback Defaults
When brand info is unavailable, the AI's instinct is to shrink fonts and add padding. That instinct is wrong for video. Apply these instead:
These kick in automatically when no mockup, screenshot, or website is provided. They ensure a professional result even with zero brand info.
Layout
justifyContent: "center"+alignItems: "center"on every scene's AbsoluteFill — no exceptionsmarginTop: -50to pull content slightly above true center- Mockup/card width: 1200px (use the upper end — fill the frame)
- Container padding: max 40px — never more
- Content must occupy at least 75% of the 1080px height. If it doesn't, increase font sizes or reduce gaps.
Font sizes (fallback — upper end of range)
| Element | Fallback size |
|---|---|
| Main headline | 140px |
| Sub-headline | 56px |
| Body / description | 36px |
| Label / pill | 22px |
| Card content text | 28px |
Colors (fallback palette — safe and professional)
- Background:
#0a0a0f(near-black, neutral) - Accent:
#6366f1(indigo — readable, modern, not product-specific) - Text:
#ffffff - Muted text:
rgba(255,255,255,0.55) - Card bg:
rgba(255,255,255,0.06)
These are STARTING POINTS only. The moment the user shares brand colors, replace them entirely.
What NOT to do without brand info
- Do NOT use neon/cyber colors — that's a personal aesthetic, not a brand match
- Do NOT use
paddingTop: 120and call it "centered" — it's top-aligned with dead space - Do NOT pick
fontSize: 32for a headline because it "looks clean" — it's illegible on video
Rule #1 — Canvas is NOT a Website
This is the single most common mistake in AI-generated Remotion videos.
Remotion canvas is 1920×1080px. Design to fill it — not like a webpage.
⚠️ These are MINIMUMS. Writing
fontSize: 18for body text is a BUG, not a style choice. Anything below the minimum column means the text is unreadable on a 1080p screen. Treat these numbers like type errors — they must be fixed before moving on.
| Element | MINIMUM (hard floor) | Recommended Range |
|---|---|---|
| Main headline | 80px | 100–160px |
| Sub-headline | 40px | 48–72px |
| Body / description | 28px | 32–40px |
| Label / pill / badge | 18px | 20–24px |
| Card content text | 22px | 24–32px |
Rules:
- Content must feel like it fills the screen
- Mockup cards:
width: 1100–1200px, centered - Container padding: max 48px — NOT 80–120px like a webpage
- If text feels "too big for comfort" — it's probably right for video
Rule #2 — All Animations MUST Use useCurrentFrame()
// CORRECT
const frame = useCurrentFrame();
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// FORBIDDEN — will NOT render correctly
// CSS: transition: opacity 1s ease
// Tailwind: animate-fade-in
// CSS @keyframes
Always include extrapolateLeft: "clamp" and extrapolateRight: "clamp" on every interpolate() call unless you explicitly need unbounded values. Forgetting this causes values to bleed outside their range and creates subtle bugs.
Rule #3 — Determinism
// FORBIDDEN — different value on every render pass
const x = Math.random() * 100;
// CORRECT — same value every time, every frame
import { random } from "remotion";
const x = random("particle-x-1") * 100; // string seed = deterministic
Rule #4 — Assets
import { staticFile } from "remotion";
import { Img } from "remotion"; // NOT native <img>
import { Audio } from "@remotion/media";
// Always use staticFile() for anything in public/ folder
<Audio src={staticFile("bg-music.mp3")} />
<Img src={staticFile("logo.png")} /> // <Img> ensures asset loads before frame renders
// Remote URLs don't need staticFile() but require CORS
Rule #5 — Smooth Preview Playback
Add premountFor={30} on <Sequence> components so heavy components preload 30 frames before their time:
<Sequence from={90} durationInFrames={120} premountFor={30}>
<HeavyScene />
</Sequence>
Easing Recipes
import { Easing } from "remotion";
// Crisp UI entrance (strong ease-out, no overshoot) — most enter animations
Easing.bezier(0.16, 1, 0.3, 1)
// Slow editorial fade (balanced ease-in-out) — mood transitions
Easing.bezier(0.45, 0, 0.55, 1)
// Playful overshoot — bouncy/fun elements
Easing.bezier(0.34, 1.56, 0.64, 1)
// Spring — no bounce (mockup entries, cards, UI)
spring({ frame, fps, config: { damping: 200 } })
// Spring — snappy with slight bounce (punchy elements)
spring({ frame, fps, config: { damping: 20, stiffness: 200 } })
// Spring — bouncy/playful
spring({ frame, fps, config: { damping: 8 } })
Pre-Layout Gates — Run BEFORE Writing Any Scene Layout
These are not suggestions. Run all three gates before writing the first <div> of any scene. If any gate fails, change the layout — never patch with fixed dimensions or extra content.
Gate 1 — Canvas Fill (both axes, mandatory)
Width axis:
dead_side = (1920 − content_width) / 2
MUST be ≤ 300px. If > 300px → content is too narrow. Widen it or change layout.
Height axis:
fill_h = content_height / 1080
MUST be ≥ 0.80. If < 0.80 → content is too short. Change the layout shape.
⚠️ If height fails, do NOT fix it by setting a fixed height on cards or containers. A fixed height creates an empty void inside the card — the dead space moves inside the element instead of being eliminated. The only real fix is changing the layout shape (Gate 2).
Real failure example — 3 short feature cards:
| Version | content_width | dead_side | content_height | fill_h | Verdict |
|---|---|---|---|---|---|
| v1: 3-col cards (365px) | 1151px | 384px | 416px | 38% | ❌ both axes fail |
v2: forced height: 720px | 1542px | 189px | 864px | 80% | ✅ width, ❌ visual (empty void in card) |
| v3: horizontal rows (1400px) | 1400px | 260px | 916px | 85% | ✅ both axes pass |
Gate 2 — Layout Selection Decision Table
Content volume determines layout shape. Choose layout from this table BEFORE writing code.
| Items | Content per item | Correct layout | Why |
|---|---|---|---|
| 3–5 items | icon + name + 1 line | Horizontal rows (1400px wide) | Short content stacks vertically → fills height naturally |
| 3 items | icon + name + 2–3 lines | 3-column cards (min 1100px wide) | Enough content to fill card height without void |
| 2 items | substantial | Side-by-side large cards or split-screen | |
| 1 item | anything | Full-screen hero |
If the natural layout for your content leaves either axis below threshold after Gate 1 — that layout is wrong. Pick a different row in this table.
Never choose layout by aesthetics or default instinct. Content fit first. Aesthetics second.
Gate 3 — Content Density Limit
Per item maximum: icon + name + 1 short line. Hard stop. No bullets. No sub-sections. No paragraphs. No second description lines.
A viewer has ~5 seconds per scene. They read 1 line. They do not read 4 bullets.
If content feels thin and you want to add more to fill space — that is Gate 2 telling you the layout shape is wrong, not that content is missing.
Fill empty space with SIZE (bigger fonts, bigger icons, larger gaps between items) — never with more content.
Universal Scene Rules
-
Vertical alignment —
justifyContent: "center"+marginTop: -40to-60(slightly above true center = modern, premium feel. NOT top-aligned with padding.)❌ WRONG — top-aligned, leaves empty space at bottom:
<AbsoluteFill style={{ display: "flex", flexDirection: "column", paddingTop: 110 }}> <h1>Headline</h1> <p>Subtext</p> {/* large empty void at bottom */} </AbsoluteFill>✅ RIGHT — vertically centered, slightly above midpoint:
<AbsoluteFill style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", marginTop: -50 }}> <h1>Headline</h1> <p>Subtext</p> {/* content fills the frame — no dead space */} </AbsoluteFill> -
Canvas fill — run Pre-Layout Gates before writing this scene. Gate 1 checks both width (
dead_side ≤ 300px) and height (fill_h ≥ 0.80) together. Gate 2 selects the layout shape from the decision table. Gate 3 caps content per item. All three gates live in the Pre-Layout Gates section above — they supersede the old width-only check that was here. -
Headline z-index — always
zIndex: 20on headline container so mockup never overlaps text -
Headline overflow — always
whiteSpace: "nowrap"on headline text (prevents wrap behind mockup) -
No
overflow: hiddenon AbsoluteFill — clips cursor and SVG elements. Remove it or place cursor outside. -
15 frame hold — every scene must have at least 15 frames of fully-visible hold at the END before transition fires. Never start a transition while an animation is still playing in the outgoing scene.
-
Font — use one consistent font throughout. Recommended: Plus Jakarta Sans via
@remotion/google-fonts. Max available weight: 800 (writingfontWeight: 900renders as 800). -
Color — 2–3 brand colors max. One dark background, one primary accent, white/muted white for text. Consistent across all scenes.
Typography Hierarchy
// Supertitle / Label pill
{ fontSize: 20, letterSpacing: "4px", textTransform: "uppercase", fontWeight: 600 }
// Main headline
{ fontSize: 112, fontWeight: 800 } // range: 100–160px
// Sub-text / description
{ fontSize: 36, opacity: 0.7 } // range: 32–40px
// Trust line / small badges
{ fontSize: 26, opacity: 0.5 } // range: 24–28px
Font Setup
import { loadFont } from "@remotion/google-fonts/PlusJakartaSans";
// Call at top level — must load before render
const { fontFamily } = loadFont("normal", {
weights: ["400", "600", "800"],
subsets: ["latin"],
});
For other fonts: install via @remotion/google-fonts/FontName or drop .woff2 in public/ and use loadFont from @remotion/fonts.
Scene Structure Template
Every scene follows this fixed layer order (bottom → top):
<AbsoluteFill style={{ background: brandBg }}