Remotion spring physics for motion graphics video production. Bouncy entrances, elastic trails, orchestrated sequences, physics presets, and organic motion patterns that interpolate() alone cannot achieve.
Use this skill when creating Remotion video compositions that need spring physics -- natural, organic motion with bounce, overshoot, and elastic settling.
Use spring when you need:
in - out)Use Remotion native interpolate() when:
Use GSAP (gsap-animation skill) when:
Note: @react-spring/web is NOT compatible with Remotion (it uses requestAnimationFrame internally). This skill uses Remotion's native spring() function which provides the same physics model in a frame-deterministic way.
Returns a value from 0 to 1 (can overshoot past 1 with low damping) based on spring physics simulation.
import { spring, useCurrentFrame, useVideoConfig } from 'remotion';
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const value = spring({
frame,
fps,
config: {
damping: 10, // 1-200: higher = less bounce
stiffness: 100, // 1-200: higher = faster snap
mass: 1, // 0.1-5: higher = more inertia
},
});
| Parameter | Range | Default | Effect |
|---|---|---|---|
damping | 1-200 | 10 | Resistance. Low = bouncy, high = smooth |
stiffness | 1-200 | 100 | Snap speed. High = fast, low = slow |
mass | 0.1-5 | 1 | Weight/inertia. High = sluggish, low = light |
overshootClamping | bool | false | Clamp at target (no overshoot) |
| Option | Type | Effect |
|---|---|---|
delay | number | Delay start by N frames (returns 0 until delay elapses) |
durationInFrames | number | Force spring to settle within N frames |
reverse | bool | Animate from 1 to 0 |
from | number | Starting value (default 0) |
to | number | Ending value (default 1) |
Calculate how many frames a spring config takes to settle. Essential for <Sequence> and composition duration.
import { measureSpring } from 'remotion';
const frames = measureSpring({
fps: 30,
config: { damping: 10, stiffness: 100 },
}); // => number of frames until settled
// src/spring-presets.ts
import { SpringConfig } from 'remotion';
export const SPRING = {
// Smooth, no bounce -- subtle reveals, background motion
smooth: { damping: 200 } as Partial<SpringConfig>,
// Snappy, minimal bounce -- UI elements, clean entrances
snappy: { damping: 20, stiffness: 200 } as Partial<SpringConfig>,
// Bouncy -- playful entrances, attention-grabbing
bouncy: { damping: 8 } as Partial<SpringConfig>,
// Heavy, slow -- dramatic reveals, weighty objects
heavy: { damping: 15, stiffness: 80, mass: 2 } as Partial<SpringConfig>,
// Wobbly -- elastic, cartoon-like overshoot
wobbly: { damping: 4, stiffness: 80 } as Partial<SpringConfig>,
// Stiff -- fast snap with tiny bounce
stiff: { damping: 15, stiffness: 300 } as Partial<SpringConfig>,
// Gentle -- slow, dreamy, organic
gentle: { damping: 20, stiffness: 40, mass: 1.5 } as Partial<SpringConfig>,
// Molasses -- very slow, heavy, barely bounces
molasses: { damping: 25, stiffness: 30, mass: 3 } as Partial<SpringConfig>,
// Pop -- strong overshoot for scale-in effects
pop: { damping: 6, stiffness: 150 } as Partial<SpringConfig>,
// Rubber -- exaggerated elastic bounce
rubber: { damping: 3, stiffness: 100, mass: 0.5 } as Partial<SpringConfig>,
} as const;
| Preset | Bounce | Speed | Feel | Best For |
|---|---|---|---|---|
smooth | None | Medium | Butter | Background, subtle reveals |
snappy | Minimal | Fast | Crisp | UI elements, buttons |
bouncy | Strong | Medium | Playful | Titles, icons, attention |
heavy | Small | Slow | Weighty | Dramatic reveals, large objects |
wobbly | Extreme | Medium | Cartoon | Playful, humorous |
stiff | Tiny | Very fast | Mechanical | Data viz, precise motion |
gentle | Minimal | Slow | Dreamy | Luxury, calm, organic |
molasses | Almost none | Very slow | Heavy | Cinematic, suspense |
pop | Strong | Fast | Punchy | Scale-in, badge, icon pop |
rubber | Extreme | Fast | Elastic | Exaggerated, cartoon, fun |
import { spring, interpolate, useCurrentFrame, useVideoConfig, AbsoluteFill } from 'remotion';
import { SPRING } from './spring-presets';
const SpringEntrance: React.FC<{
children: React.ReactNode;
preset?: keyof typeof SPRING;
delay?: number;
}> = ({ children, preset = 'bouncy', delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, delay, config: SPRING[preset] });
const translateY = interpolate(progress, [0, 1], [60, 0]);
return (
<AbsoluteFill style={{
opacity: progress,
transform: `translateY(${translateY}px)`,
}}>
{children}
</AbsoluteFill>
);
};
const ScalePop: React.FC<{
children: React.ReactNode;
delay?: number;
}> = ({ children, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// pop preset overshoots past 1, creating natural scale bounce
const scale = spring({ frame, fps, delay, config: SPRING.pop });
const opacity = spring({ frame, fps, delay, config: SPRING.smooth });
return (
<div style={{
transform: `scale(${scale})`,
opacity,
}}>
{children}
</div>
);
};
const EnterExit: React.FC<{
children: React.ReactNode;
enterDelay?: number;
exitBeforeEnd?: number; // frames before composition end to start exit
}> = ({ children, enterDelay = 0, exitBeforeEnd = 30 }) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const enter = spring({ frame, fps, delay: enterDelay, config: SPRING.bouncy });
const exit = spring({
frame, fps,
delay: durationInFrames - exitBeforeEnd,
config: SPRING.snappy,
});
const scale = enter - exit; // 0 -> 1 -> 0
const opacity = enter - exit;
return (
<div style={{ transform: `scale(${scale})`, opacity }}>
{children}
</div>
);
};
Mimics React Spring's useTrail -- each element enters with a frame delay.
const SpringTrail: React.FC<{
items: React.ReactNode[];
staggerFrames?: number;
preset?: keyof typeof SPRING;
direction?: 'up' | 'down' | 'left' | 'right';
}> = ({ items, staggerFrames = 4, preset = 'bouncy', direction = 'up' }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const getOffset = (progress: number) => {
const distance = 50;
const remaining = interpolate(progress, [0, 1], [distance, 0]);
switch (direction) {
case 'up': return { transform: `translateY(${remaining}px)` };
case 'down': return { transform: `translateY(${-remaining}px)` };
case 'left': return { transform: `translateX(${remaining}px)` };
case 'right': return { transform: `translateX(${-remaining}px)` };
}
};
return (
<>
{items.map((item, i) => {
const delay = i * staggerFrames;
const progress = spring({ frame, fps, delay, config: SPRING[preset] });
return (
<div key={i} style={{ opacity: progress, ...getOffset(progress) }}>
{item}
</div>
);
})}
</>
);
};
Manual character splitting with spring stagger. For advanced text splitting (mask reveals, line wrapping), use gsap-animation skill instead.
const CharacterTrail: React.FC<{
text: string;
staggerFrames?: number;
preset?: keyof typeof SPRING;
fontSize?: number;
}> = ({ text, staggerFrames = 2, preset = 'pop', fontSize = 80 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<div style={{ display: 'flex', justifyContent: 'center', overflow: 'hidden' }}>
{text.split('').map((char, i) => {
const delay = i * staggerFrames;
const progress = spring({ frame, fps, delay, config: SPRING[preset] });
const translateY = interpolate(progress, [0, 1], [fontSize, 0]);
return (
<span key={i} style={{
display: 'inline-block',
fontSize,
fontWeight: 'bold',
color: '#fff',
opacity: progress,
transform: `translateY(${translateY}px)`,
whiteSpace: 'pre',
}}>
{char === ' ' ? '\u00A0' : char}
</span>
);
})}
</div>
);
};
const WordTrail: React.FC<{
text: string;
staggerFrames?: number;
preset?: keyof typeof SPRING;
fontSize?: number;
}> = ({ text, staggerFrames = 5, preset = 'bouncy', fontSize = 64 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const words = text.split(' ');
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.3em', justifyContent: 'center' }}>
{words.map((word, i) => {
const delay = i * staggerFrames;
const progress = spring({ frame, fps, delay, config: SPRING[preset] });
const scale = spring({ frame, fps, delay, config: SPRING.pop });
return (
<span key={i} style={{
display: 'inline-block',
fontSize,
fontWeight: 'bold',
color: '#fff',
opacity: progress,
transform: `scale(${scale})`,
}}>
{word}
</span>
);
})}
</div>
);
};
const GridStagger: React.FC<{
items: React.ReactNode[];
columns: number;
cellSize?: number;
gap?: number;
}> = ({ items, columns, cellSize = 120, gap = 16 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const rows = Math.ceil(items.length / columns);
const centerCol = (columns - 1) / 2;
const centerRow = (rows - 1) / 2;
return (
<div style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, ${cellSize}px)`,
gap,
}}>
{items.map((item, i) => {
const col = i % columns;
const row = Math.floor(i / columns);
// Distance from center determines delay
const dist = Math.sqrt((col - centerCol) ** 2 + (row - centerRow) ** 2);
const delay = Math.round(dist * 4);
const progress = spring({ frame, fps, delay, config: SPRING.pop });
return (
<div key={i} style={{
width: cellSize, height: cellSize,
opacity: progress,
transform: `scale(${progress})`,
}}>
{item}
</div>
);
})}
</div>
);
};
Mimics React Spring's useChain -- animations trigger in sequence using measureSpring for timing.
import { spring, measureSpring, interpolate, useCurrentFrame, useVideoConfig } from 'remotion';
const SpringChain: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Step 1: Container scales in
const step1Config = SPRING.bouncy;
const step1 = spring({ frame, fps, config: step1Config });
const step1Duration = measureSpring({ fps, config: step1Config });
// Step 2: Title fades up (starts when step1 is 80% done)
const step2Delay = Math.round(step1Duration * 0.8);
const step2Config = SPRING.snappy;
const step2 = spring({ frame, fps, delay: step2Delay, config: step2Config });
const step2Duration = measureSpring({ fps, config: step2Config });
// Step 3: Subtitle appears (starts when step2 finishes)
const step3Delay = step2Delay + step2Duration;
const step3 = spring({ frame, fps, delay: step3Delay, config: SPRING.gentle });
return (
<AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{/* Container */}
<div style={{
transform: `scale(${step1})`,
opacity: step1,
background: '#1e293b',
padding: 60,
borderRadius: 24,
textAlign: 'center',
}}>
{/* Title */}
<h1 style={{
fontSize: 72, fontWeight: 'bold', color: '#fff',
opacity: step2,
transform: `translateY(${interpolate(step2, [0, 1], [30, 0])}px)`,
}}>Spring Chain</h1>
{/* Subtitle */}
<p style={{
fontSize: 28, color: 'rgba(255,255,255,0.7)', marginTop: 16,
opacity: step3,
transform: `translateY(${interpolate(step3, [0, 1], [20, 0])}px)`,
}}>Sequential spring orchestration</p>
</div>
</AbsoluteFill>
);
};
Reusable hook for chaining multiple springs with overlap control.
import { spring, measureSpring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';
type ChainStep = {
config: Partial<SpringConfig>;
overlap?: number; // 0-1, how much to overlap with previous step (default 0)
};
function useSpringChain(steps: ChainStep[]) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
let currentDelay = 0;
return steps.map((step, i) => {
if (i > 0) {
const prevDuration = measureSpring({ fps, config: steps[i - 1].config });
const overlap = step.overlap ?? 0;
currentDelay += Math.round(prevDuration * (1 - overlap));
}
return spring({ frame, fps, delay: currentDelay, config: step.config });
});
}
// Usage:
const [container, title, subtitle, cta] = useSpringChain([
{ config: SPRING.bouncy },
{ config: SPRING.snappy, overlap: 0.2 },
{ config: SPRING.gentle, overlap: 0.3 },
{ config: SPRING.pop, overlap: 0.1 },
]);
const MultiPropertySpring: React.FC<{ delay?: number }> = ({ delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Position: snappy (arrives fast)
const position = spring({ frame, fps, delay, config: SPRING.snappy });
// Scale: bouncy (overshoots then settles)
const scale = spring({ frame, fps, delay, config: SPRING.bouncy });
// Rotation: wobbly (elastic wobble)
const rotation = spring({ frame, fps, delay, config: SPRING.wobbly });
// Opacity: smooth (no bounce)
const opacity = spring({ frame, fps, delay, config: SPRING.smooth });
const translateX = interpolate(position, [0, 1], [-300, 0]);
const rotate = interpolate(rotation, [0, 1], [-15, 0]);
return (
<div style={{
transform: `translateX(${translateX}px) scale(${scale}) rotate(${rotate}deg)`,
opacity,
}}>
Multi-Property
</div>
);
};
Number counter with spring physics -- overshoots the target then settles.
const SpringCounter: React.FC<{
endValue: number;
prefix?: string;
suffix?: string;
preset?: keyof typeof SPRING;
delay?: number;
}> = ({ endValue, prefix = '', suffix = '', preset = 'bouncy', delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, delay, config: SPRING[preset] });
// With bouncy config, progress overshoots past 1.0 before settling
// This means the counter briefly shows a number > endValue, then settles
const value = Math.round(progress * endValue);
return (
<div style={{
fontSize: 96, fontWeight: 'bold', color: '#fff',
fontVariantNumeric: 'tabular-nums',
}}>
{prefix}{value.toLocaleString()}{suffix}
</div>
);
};
Comparison with linear counter:
interpolate() counter: smoothly reaches exact target, no overshootconst SpringCardFlip: React.FC<{
frontContent: React.ReactNode;
backContent: React.ReactNode;
flipDelay?: number;
}> = ({ frontContent, backContent, flipDelay = 15 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const flipProgress = spring({
frame, fps, delay: flipDelay,
config: { damping: 15, stiffness: 80 }, // slow, weighty flip
});
const rotateY = interpolate(flipProgress, [0, 1], [0, 180]);
return (
<AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ perspective: 800 }}>
<div style={{
width: 500, height: 320, position: 'relative',
transformStyle: 'preserve-3d',
transform: `rotateY(${rotateY}deg)`,
}}>
<div style={{
position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
background: '#1e293b', borderRadius: 16,
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
}}>{frontContent}</div>
<div style={{
position: 'absolute', inset: 0, backfaceVisibility: 'hidden',
background: '#3b82f6', borderRadius: 16, transform: 'rotateY(180deg)',
display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 32,
}}>{backContent}</div>
</div>
</div>
</AbsoluteFill>
);
};
const PerspectiveTilt: React.FC<{
children: React.ReactNode;
rotateX?: number;
rotateY?: number;
delay?: number;
}> = ({ children, rotateX = -20, rotateY = 15, delay = 0 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = spring({ frame, fps, delay, config: SPRING.heavy });
const rx = interpolate(progress, [0, 1], [rotateX, 0]);
const ry = interpolate(progress, [0, 1], [rotateY, 0]);
const translateZ = interpolate(progress, [0, 1], [-200, 0]);
return (
<div style={{
perspective: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{
transform: `perspective(1000px) rotateX(${rx}deg) rotateY(${ry}deg) translateZ(${translateZ}px)`,
opacity: progress,
}}>
{children}
</div>
</div>
);
};
const SpringCrossfade: React.FC<{
outgoing: React.ReactNode;
incoming: React.ReactNode;
switchFrame: number;
}> = ({ outgoing, incoming, switchFrame }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const outOpacity = frame < switchFrame ? 1 : 1 - spring({
frame: frame - switchFrame, fps, config: SPRING.smooth,
});
const inOpacity = frame < switchFrame ? 0 : spring({
frame: frame - switchFrame, fps, config: SPRING.smooth,
});
const inScale = frame < switchFrame ? 0.95 : interpolate(
spring({ frame: frame - switchFrame, fps, config: SPRING.bouncy }),
[0, 1], [0.95, 1]
);
return (
<AbsoluteFill>
<AbsoluteFill style={{ opacity: outOpacity }}>{outgoing}</AbsoluteFill>
<AbsoluteFill style={{ opacity: inOpacity, transform: `scale(${inScale})` }}>
{incoming}
</AbsoluteFill>
</AbsoluteFill>
);
};
const SpringSlide: React.FC<{
outgoing: React.ReactNode;
incoming: React.ReactNode;
switchFrame: number;
direction?: 'left' | 'right' | 'up' | 'down';
}> = ({ outgoing, incoming, switchFrame, direction = 'left' }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const progress = frame < switchFrame ? 0 : spring({
frame: frame - switchFrame, fps, config: SPRING.snappy,
});
const getTransform = (isOutgoing: boolean) => {
const offset = isOutgoing ? interpolate(progress, [0, 1], [0, -100]) : interpolate(progress, [0, 1], [100, 0]);
switch (direction) {
case 'left': return `translateX(${offset}%)`;
case 'right': return `translateX(${-offset}%)`;
case 'up': return `translateY(${offset}%)`;
case 'down': return `translateY(${-offset}%)`;
}
};
return (
<AbsoluteFill style={{ overflow: 'hidden' }}>
<AbsoluteFill style={{ transform: getTransform(true) }}>{outgoing}</AbsoluteFill>
<AbsoluteFill style={{ transform: getTransform(false) }}>{incoming}</AbsoluteFill>
</AbsoluteFill>
);
};
const SpringTitleCard: React.FC<{
title: string;
subtitle?: string;
accent?: string;
}> = ({ title, subtitle, accent = '#3b82f6' }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// Background shape
const bgScale = spring({ frame, fps, config: SPRING.heavy });
// Title words stagger
const words = title.split(' ');
// Divider
const dividerWidth = spring({ frame, fps, delay: 8, config: SPRING.snappy });
// Subtitle
const subtitleProgress = spring({ frame, fps, delay: 15, config: SPRING.gentle });
return (
<AbsoluteFill style={{
background: '#0f172a',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{/* Accent circle */}
<div style={{
position: 'absolute', width: 300, height: 300, borderRadius: '50%',
background: `${accent}20`, transform: `scale(${bgScale})`,
}} />
<div style={{ textAlign: 'center', position: 'relative', zIndex: 1 }}>
{/* Title with word stagger */}
<div style={{ display: 'flex', gap: '0.3em', justifyContent: 'center', flexWrap: 'wrap' }}>
{words.map((word, i) => {
const delay = i * 4;
const progress = spring({ frame, fps, delay, config: SPRING.pop });
const y = interpolate(progress, [0, 1], [40, 0]);
return (
<span key={i} style={{
fontSize: 80, fontWeight: 'bold', color: '#fff',
display: 'inline-block',
opacity: progress,
transform: `translateY(${y}px) scale(${progress})`,
}}>{word}</span>
);
})}
</div>
{/* Divider */}
<div style={{
width: 80, height: 3, background: accent, margin: '20px auto',
transform: `scaleX(${dividerWidth})`, transformOrigin: 'center',
}} />
{/* Subtitle */}
{subtitle && (
<p style={{
fontSize: 28, color: 'rgba(255,255,255,0.7)',
opacity: subtitleProgress,
transform: `translateY(${interpolate(subtitleProgress, [0, 1], [15, 0])}px)`,
}}>{subtitle}</p>
)}
</div>
</AbsoluteFill>
);
};
const SpringLowerThird: React.FC<{
name: string;
title: string;
accent?: string;
hold?: number;
}> = ({ name, title, accent = '#3b82f6', hold = 90 }) => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Enter
const barIn = spring({ frame, fps, config: SPRING.snappy });
const nameIn = spring({ frame, fps, delay: 6, config: SPRING.bouncy });
const titleIn = spring({ frame, fps, delay: 10, config: SPRING.gentle });
// Exit (spring math subtraction)
const exitDelay = durationInFrames - 20;
const barOut = spring({ frame, fps, delay: exitDelay, config: SPRING.stiff });
const nameOut = spring({ frame, fps, delay: exitDelay - 4, config: SPRING.stiff });
const titleOut = spring({ frame, fps, delay: exitDelay - 8, config: SPRING.stiff });
return (
<AbsoluteFill>
<div style={{ position: 'absolute', bottom: 80, left: 60 }}>
{/* Bar */}
<div style={{
background: accent, padding: '12px 24px', borderRadius: 4,
transform: `scaleX(${barIn - barOut})`,
transformOrigin: 'left',
opacity: barIn - barOut,
}}>
<div style={{
fontSize: 28, fontWeight: 'bold', color: '#fff',
opacity: nameIn - nameOut,
transform: `translateX(${interpolate(nameIn - nameOut, [0, 1], [-20, 0])}px)`,
}}>{name}</div>
<div style={{
fontSize: 18, color: 'rgba(255,255,255,0.8)',
opacity: titleIn - titleOut,
transform: `translateX(${interpolate(titleIn - titleOut, [0, 1], [-15, 0])}px)`,
}}>{title}</div>
</div>
</div>
</AbsoluteFill>
);
};
const SpringFeatureGrid: React.FC<{
features: Array<{ icon: string; label: string }>;
columns?: number;
}> = ({ features, columns = 3 }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return (
<AbsoluteFill style={{
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: '#0f172a',
}}>
<div style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 200px)`,
gap: 32,
}}>
{features.map(({ icon, label }, i) => {
const delay = i * 5;
const scale = spring({ frame, fps, delay, config: SPRING.pop });
const opacity = spring({ frame, fps, delay, config: SPRING.smooth });
return (
<div key={i} style={{
textAlign: 'center', padding: 24,
background: 'rgba(255,255,255,0.05)', borderRadius: 16,
transform: `scale(${scale})`, opacity,
}}>
<div style={{ fontSize: 48 }}>{icon}</div>
<div style={{ fontSize: 18, color: '#fff', marginTop: 12 }}>{label}</div>
</div>
);
})}
</div>
</AbsoluteFill>
);
};
const SpringOutro: React.FC<{
headline: string;
tagline?: string;
ctaText?: string;
accent?: string;
}> = ({ headline, tagline, ctaText, accent = '#3b82f6' }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const headlineProgress = spring({ frame, fps, config: SPRING.heavy });
const taglineProgress = spring({ frame, fps, delay: 12, config: SPRING.gentle });
const ctaProgress = spring({ frame, fps, delay: 20, config: SPRING.pop });
return (
<AbsoluteFill style={{
background: '#0f172a',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ textAlign: 'center' }}>
<h1 style={{
fontSize: 72, fontWeight: 'bold', color: '#fff',
opacity: headlineProgress,
transform: `scale(${headlineProgress})`,
}}>{headline}</h1>
{tagline && (
<p style={{
fontSize: 28, color: 'rgba(255,255,255,0.6)', marginTop: 16,
opacity: taglineProgress,
transform: `translateY(${interpolate(taglineProgress, [0, 1], [15, 0])}px)`,
}}>{tagline}</p>
)}
{ctaText && (
<div style={{
display: 'inline-block', marginTop: 32,
background: accent, padding: '16px 40px', borderRadius: 8,
fontSize: 24, fontWeight: 'bold', color: '#fff',
transform: `scale(${ctaProgress})`, opacity: ctaProgress,
}}>{ctaText}</div>
)}
</div>
</AbsoluteFill>
);
};
Reusable hook for trail animations.
import { spring, SpringConfig, useCurrentFrame, useVideoConfig } from 'remotion';
function useSpringTrail(
count: number,
config: Partial<SpringConfig>,
staggerFrames = 4,
baseDelay = 0,
) {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
return Array.from({ length: count }, (_, i) => {
const delay = baseDelay + i * staggerFrames;
return spring({ frame, fps, delay, config });
});
}
// Usage:
const trail = useSpringTrail(5, SPRING.pop, 4);
// trail = [0.98, 0.85, 0.5, 0.1, 0] -- each item at different progress
Reusable hook for enter + exit pattern.
function useSpringEnterExit(
enterConfig: Partial<SpringConfig>,
exitConfig: Partial<SpringConfig>,
enterDelay = 0,
exitBeforeEnd = 30,
) {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const enter = spring({ frame, fps, delay: enterDelay, config: enterConfig });
const exit = spring({
frame, fps,
delay: durationInFrames - exitBeforeEnd,
config: exitConfig,
});
return enter - exit;
}
// Usage:
const progress = useSpringEnterExit(SPRING.bouncy, SPRING.stiff, 0, 25);
const CombinedScene: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const bgOpacity = spring({ frame, fps, config: SPRING.smooth });
return (
<AbsoluteFill>
{/* react-animation: visual atmosphere */}
<div style={{ opacity: bgOpacity }}>
<Aurora colorStops={['#3A29FF', '#FF94B4']} />
</div>
{/* spring-animation: bouncy title entrance */}
<SpringTitleCard title="Natural Motion" subtitle="Physics-driven beauty" />
{/* gsap-animation: text splitting that spring can't do */}
<GSAPTextReveal text="Advanced Typography" />
</AbsoluteFill>
);
};
| Skill | Best For |
|---|---|
| spring-animation | Bouncy entrances, elastic trails, organic physics, overshoot effects, spring counters |
| gsap-animation | Text splitting (SplitText), SVG drawing (DrawSVG), SVG morphing, complex timeline labels |
| react-animation | Visual backgrounds (Aurora, Silk, Particles), shader effects |
export const RemotionRoot: React.FC = () => (
<>
<Composition id="SpringTitleCard" component={SpringTitleCard}
durationInFrames={90} fps={30} width={1920} height={1080}
defaultProps={{ title: 'SPRING PHYSICS', subtitle: 'Natural motion for video' }} />
<Composition id="SpringLowerThird" component={SpringLowerThird}
durationInFrames={180} fps={30} width={1920} height={1080}
defaultProps={{ name: 'Jane Smith', title: 'Creative Director' }} />
<Composition id="SpringFeatureGrid" component={SpringFeatureGrid}
durationInFrames={90} fps={30} width={1920} height={1080}
defaultProps={{ features: [
{ icon: '🚀', label: 'Fast' },
{ icon: '🎯', label: 'Precise' },
{ icon: '✨', label: 'Beautiful' },
]}} />
<Composition id="SpringOutro" component={SpringOutro}
durationInFrames={120} fps={30} width={1920} height={1080}
defaultProps={{ headline: 'GET STARTED', tagline: 'Try it free today', ctaText: 'Sign Up →' }} />
</>
);
# Default MP4
npx remotion render src/index.ts SpringTitleCard --output out/title.mp4
# High quality
npx remotion render src/index.ts SpringTitleCard --codec h264 --crf 15
# GIF
npx remotion render src/index.ts SpringTitleCard --codec gif --every-nth-frame 2
# ProRes for editing
npx remotion render src/index.ts SpringTitleCard --codec prores --prores-profile 4444