Thank-You Video (Remotion)
A warm, heartfelt 4-second thank-you animation built with Remotion featuring a bold serif headline that springs upward, a gold decorative rule that sweeps in, a subtitle and tagline that fade into view, a beating heart icon with a realistic pulse rhythm, softly rising particles, star sparkles scattered around the frame, and a late-entering lens flare overlay — all layered over a rich peach-to-coral-to-rose gradient background.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Composition,
Easing,
Sequence,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
} from "remotion";
// ─── Palette ────────────────────────────────────────────────────────────────
const PALETTE = {
peach: "#FDDAB5",
coral: "#F4845F",
rose: "#E8687A",
gold: "#D4A843",
ivory: "#FFF8F0",
softPink: "#F9C6C9",
warmWhite: "#FFFAF5",
deepRose: "#C0435A",
};
// ─── FloatingParticle ────────────────────────────────────────────────────────
interface ParticleProps {
x: number;
startY: number;
size: number;
delay: number;
color: string;
opacity: number;
}
const FloatingParticle: React.FC<ParticleProps> = ({
x,
startY,
size,
delay,
color,
opacity,
}) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = Math.max(0, (frame - delay) / (durationInFrames - delay));
const y = startY - progress * 320;
const wobble = Math.sin((frame + delay * 3) * 0.06) * 18;
const fade = interpolate(progress, [0, 0.1, 0.8, 1], [0, opacity, opacity, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
left: x + wobble,
top: y,
width: size,
height: size,
borderRadius: "50%",
background: color,
opacity: fade,
pointerEvents: "none",
}}
/>
);
};
// ─── HeartIcon ───────────────────────────────────────────────────────────────
const HeartIcon: React.FC<{ size: number; color: string }> = ({ size, color }) => {
const frame = useCurrentFrame();
const beatCycle = (frame % 30) / 30;
const beat =
beatCycle < 0.15
? interpolate(beatCycle, [0, 0.08, 0.15], [1, 1.22, 1.08], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
})
: interpolate(beatCycle, [0.15, 0.28, 1], [1.08, 0.96, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
width: size,
height: size,
transform: `scale(${beat})`,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
viewBox="0 0 100 90"
width={size}
height={size * 0.9}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M50 85 C50 85 5 55 5 28 C5 14 16 4 28 4 C36 4 44 9 50 17 C56 9 64 4 72 4 C84 4 95 14 95 28 C95 55 50 85 50 85Z"
fill={color}
stroke="rgba(255,255,255,0.4)"
strokeWidth="2"
/>
<path
d="M30 22 C24 26 20 34 22 42"
stroke="rgba(255,255,255,0.5)"
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
</div>
);
};
// ─── LensFlare ───────────────────────────────────────────────────────────────
const LensFlare: React.FC = () => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const enterProgress = interpolate(frame, [70, 100], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const pulse = Math.sin(frame * 0.12) * 0.15 + 0.85;
return (
<div
style={{
position: "absolute",
inset: 0,
pointerEvents: "none",
opacity: enterProgress * pulse * 0.55,
}}
>
{/* primary glow — top-right */}
<div
style={{
position: "absolute",
top: -120,
right: -80,
width: 560,
height: 560,
borderRadius: "50%",
background:
"radial-gradient(circle, rgba(255,230,180,0.55) 0%, rgba(244,132,95,0.18) 45%, transparent 70%)",
}}
/>
{/* secondary scatter — bottom-left */}
<div
style={{
position: "absolute",
bottom: -60,
left: -40,
width: 340,
height: 340,
borderRadius: "50%",
background:
"radial-gradient(circle, rgba(255,200,180,0.35) 0%, rgba(232,104,122,0.1) 55%, transparent 75%)",
}}
/>
{/* small specular */}
<div
style={{
position: "absolute",
top: "28%",
right: "18%",
width: 80,
height: 80,
borderRadius: "50%",
background:
"radial-gradient(circle, rgba(255,255,240,0.9) 0%, rgba(255,220,160,0.3) 50%, transparent 70%)",
}}
/>
</div>
);
};
// ─── StarSparkle ─────────────────────────────────────────────────────────────
const StarSparkle: React.FC<{ x: number; y: number; delay: number; size: number }> = ({
x,
y,
delay,
size,
}) => {
const frame = useCurrentFrame();
const localFrame = frame - delay;
const opacity = interpolate(localFrame, [0, 8, 18, 32], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const scale = interpolate(localFrame, [0, 8, 18, 32], [0.2, 1.2, 1, 0.3], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const rotate = interpolate(localFrame, [0, 32], [0, 45], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
position: "absolute",
left: x,
top: y,
opacity,
transform: `scale(${scale}) rotate(${rotate}deg)`,
transformOrigin: "center",
}}
>
<svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<path
d="M12 2L13.5 9.5L21 12L13.5 14.5L12 22L10.5 14.5L3 12L10.5 9.5L12 2Z"
fill={PALETTE.gold}
stroke="rgba(255,240,200,0.7)"
strokeWidth="0.5"
/>
</svg>
</div>
);
};
// ─── GratitudeText ───────────────────────────────────────────────────────────
const GratitudeText: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const titleSpring = spring({
frame,
fps,
config: { damping: 14, stiffness: 120, mass: 1 },
delay: 8,
});
const titleY = interpolate(titleSpring, [0, 1], [80, 0]);
const titleOpacity = interpolate(titleSpring, [0, 1], [0, 1]);
const subtitleOpacity = interpolate(frame, [38, 60], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const subtitleY = interpolate(frame, [38, 60], [20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const taglineOpacity = interpolate(frame, [55, 75], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 0,
}}
>
{/* "Thank You" */}
<div
style={{
transform: `translateY(${titleY}px)`,
opacity: titleOpacity,
}}
>
<div
style={{
fontSize: 108,
fontWeight: 800,
fontFamily: "Georgia, 'Times New Roman', serif",
color: PALETTE.ivory,
letterSpacing: "-1px",
lineHeight: 1,
textShadow: `0 4px 32px rgba(192,67,90,0.35), 0 2px 8px rgba(0,0,0,0.18)`,
WebkitTextStroke: "1px rgba(255,220,200,0.25)",
}}
>
Thank You
</div>
</div>
{/* Decorative rule */}
<div
style={{
marginTop: 16,
marginBottom: 20,
width: interpolate(frame, [30, 55], [0, 320], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
}),
height: 2,
background: `linear-gradient(90deg, transparent, ${PALETTE.gold}, rgba(255,200,150,0.6), transparent)`,
borderRadius: 1,
}}
/>
{/* Subtitle */}
<div
style={{
transform: `translateY(${subtitleY}px)`,
opacity: subtitleOpacity,
fontSize: 32,
fontWeight: 400,
fontFamily: "Georgia, serif",
color: "rgba(255,245,235,0.92)",
letterSpacing: "3px",
textTransform: "uppercase",
textShadow: "0 2px 12px rgba(0,0,0,0.2)",
}}
>
From the Stealthis Team
</div>
{/* Tagline */}
<div
style={{
marginTop: 14,
opacity: taglineOpacity,
fontSize: 18,
fontWeight: 300,
fontFamily: "system-ui, -apple-system, sans-serif",
color: "rgba(255,235,210,0.75)",
letterSpacing: "1.5px",
fontStyle: "italic",
}}
>
With gratitude & appreciation ✦
</div>
</div>
);
};
// ─── HeartSection ────────────────────────────────────────────────────────────
const HeartSection: React.FC = () => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const heartSpring = spring({
frame,
fps,
config: { damping: 11, stiffness: 160, mass: 0.8 },
delay: 50,
});
return (
<div
style={{
position: "absolute",
bottom: 80,
left: "50%",
transform: `translateX(-50%) scale(${heartSpring})`,
opacity: heartSpring,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 8,
}}
>
<HeartIcon size={72} color={PALETTE.rose} />
<div
style={{
fontSize: 13,
fontFamily: "system-ui, sans-serif",
color: "rgba(255,230,210,0.65)",
letterSpacing: "2px",
textTransform: "uppercase",
}}
>
Made with love
</div>
</div>
);
};
// ─── Background ──────────────────────────────────────────────────────────────
const Background: React.FC = () => {
const frame = useCurrentFrame();
const shiftX = Math.sin(frame * 0.018) * 30;
const shiftY = Math.cos(frame * 0.013) * 20;
return (
<AbsoluteFill>
{/* Base warm gradient */}
<div
style={{
position: "absolute",
inset: 0,
background: `linear-gradient(145deg, #FDDAB5 0%, #F4845F 45%, #E8687A 80%, #C0435A 100%)`,
}}
/>
{/* Animated overlay blob */}
<div
style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse 70% 60% at ${50 + shiftX * 0.05}% ${40 + shiftY * 0.04}%, rgba(255,240,210,0.22) 0%, transparent 70%)`,
}}
/>
{/* Warm vignette */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse 100% 100% at 50% 50%, transparent 40%, rgba(140,40,55,0.38) 100%)",
}}
/>
{/* Subtle noise texture via SVG filter workaround — thin semi-transparent overlay */}
<div
style={{
position: "absolute",
inset: 0,
opacity: 0.04,
background: `repeating-linear-gradient(
45deg,
rgba(255,255,255,0.1) 0px,
rgba(255,255,255,0.1) 1px,
transparent 1px,
transparent 8px
)`,
}}
/>
</AbsoluteFill>
);
};
// ─── ParticleField ───────────────────────────────────────────────────────────
const PARTICLES: ParticleProps[] = [
{ x: 120, startY: 680, size: 10, delay: 0, color: "rgba(255,230,200,0.7)", opacity: 0.7 },
{ x: 280, startY: 720, size: 7, delay: 5, color: "rgba(255,255,220,0.6)", opacity: 0.6 },
{ x: 450, startY: 700, size: 13, delay: 2, color: "rgba(255,200,180,0.5)", opacity: 0.5 },
{ x: 680, startY: 740, size: 8, delay: 8, color: "rgba(255,240,200,0.65)", opacity: 0.65 },
{ x: 820, startY: 710, size: 11, delay: 3, color: "rgba(255,220,190,0.55)", opacity: 0.55 },
{ x: 1050, startY: 730, size: 9, delay: 6, color: "rgba(255,245,215,0.7)", opacity: 0.7 },
{ x: 1180, startY: 690, size: 7, delay: 1, color: "rgba(255,210,180,0.6)", opacity: 0.6 },
{ x: 60, startY: 650, size: 6, delay: 10, color: "rgba(255,255,230,0.5)", opacity: 0.5 },
{ x: 960, startY: 760, size: 12, delay: 4, color: "rgba(255,230,200,0.6)", opacity: 0.6 },
{ x: 370, startY: 760, size: 8, delay: 7, color: "rgba(255,240,210,0.55)", opacity: 0.55 },
{ x: 740, startY: 780, size: 10, delay: 9, color: "rgba(255,220,200,0.65)", opacity: 0.65 },
{ x: 1120, startY: 750, size: 6, delay: 12, color: "rgba(255,245,225,0.5)", opacity: 0.5 },
];
const SPARKLES = [
{ x: 95, y: 95, delay: 20, size: 20 },
{ x: 1145, y: 80, delay: 35, size: 16 },
{ x: 1210, y: 320, delay: 55, size: 14 },
{ x: 65, y: 560, delay: 45, size: 18 },
{ x: 580, y: 40, delay: 28, size: 15 },
{ x: 1080, y: 610, delay: 62, size: 13 },
{ x: 210, y: 160, delay: 40, size: 12 },
{ x: 1020, y: 155, delay: 30, size: 17 },
];
// ─── Main Component ───────────────────────────────────────────────────────────
export const RemotionThankYou: React.FC = () => {
return (
<AbsoluteFill style={{ fontFamily: "system-ui, sans-serif" }}>
<Background />
{/* Floating particles */}
<AbsoluteFill style={{ overflow: "hidden" }}>
{PARTICLES.map((p, i) => (
<FloatingParticle key={i} {...p} />
))}
</AbsoluteFill>
{/* Star sparkles */}
<AbsoluteFill>
{SPARKLES.map((s, i) => (
<StarSparkle key={i} {...s} />
))}
</AbsoluteFill>
{/* Lens flare — enters late */}
<LensFlare />
{/* Central text block */}
<AbsoluteFill
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
paddingBottom: 60,
}}
>
<GratitudeText />
</AbsoluteFill>
{/* Heart at bottom */}
<HeartSection />
</AbsoluteFill>
);
};
// ─── Remotion Root ────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="remotion-thank-you"
component={RemotionThankYou}
durationInFrames={120}
fps={30}
width={1280}
height={720}
/>
);Thank-You Video
A warm, sincere thank-you video composition built entirely in Remotion. The design pairs a rich peach-to-coral-to-rose gradient with ivory and gold typography to evoke warmth and gratitude without feeling overly decorative. Every element is layered with care — the headline uses a heavyweight Georgia serif for authority, while the subtitle and tagline are set in lighter weights to let the main message breathe.
The animation choreography is tightly sequenced. The “Thank You” headline launches in on a spring curve (damping 14, stiffness 120) for a satisfying physical snap. A gold rule sweeps outward beneath it, followed by the subtitle sliding up and the tagline fading in. Floating bubble particles drift gently upward throughout the clip, and gold star sparkles flash at the frame edges to add subtle sparkle without distraction. A SVG-based beating heart appears near the bottom via a pop spring and maintains a realistic two-beat rhythm using a custom interpolate cycle. Finally, a multi-layered radial-gradient lens flare pulses in over the last second to add cinematic warmth.
All styles are pure React inline objects — no external CSS, no Google Fonts, no third-party packages. The composition runs at 30 fps for 4 seconds (120 frames) at 1280 × 720.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 4 s (120 frames) |
Timeline
| Time | Action |
|---|---|
| 0:00 – 0:10 (f 0–10) | Background gradient fades in; floating particles begin rising; first star sparkles flash |
| 0:10 – 0:20 (f 8–20) | “Thank You” headline springs upward; gold decorative rule sweeps outward |
| 0:20 – 0:30 (f 38–60) | “From the Stealthis Team” subtitle slides up and fades in |
| 0:25 – 0:33 (f 55–75) | Italic tagline fades in; beating heart pops in via spring (f 50) |
| 0:33 – 0:40 (f 70–100) | Lens flare overlay enters and begins slow pulse; heart continues beating rhythm |