Pricing Reveal Animation (Remotion)
Three staggered pricing cards (Starter, Pro, Enterprise) spring up from below with bounce physics, each revealing plan name, price, feature checklist, and CTA button in a sequenced cascade. The highlighted Pro card carries a pulsing Most Popular badge, a branded indigo glow, and a subtle scale-pop after all cards settle. Closes with a 14-day free trial tagline fade. Built with pure Remotion — no external animation libraries.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Composition,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Easing,
Sequence,
} from "remotion";
// ── Design tokens ─────────────────────────────────────────────────────
const BG = "#0a0a0f";
const SURFACE = "#12121a";
const CARD_BG = "#1a1a2e";
const BRAND = "#6366f1";
const BRAND_2 = "#8b5cf6";
const ACCENT = "#06b6d4";
const TEXT = "#f8fafc";
const TEXT_MUTED = "rgba(248,250,252,0.55)";
const SUCCESS = "#10b981";
const DURATION = 300; // 10 s @ 30 fps
// ── Data ──────────────────────────────────────────────────────────────
interface PricingFeature {
text: string;
}
interface PricingPlan {
id: string;
name: string;
price: string;
sub: string;
cta: string;
features: PricingFeature[];
accent: string;
glow: string;
highlighted: boolean;
}
const PLANS: PricingPlan[] = [
{
id: "starter",
name: "Starter",
price: "$29",
sub: "per month, billed annually",
cta: "Get started free",
features: [
{ text: "Up to 3 team members" },
{ text: "5 active projects" },
{ text: "10 GB storage" },
{ text: "Standard analytics" },
],
accent: ACCENT,
glow: "rgba(6,182,212,0.12)",
highlighted: false,
},
{
id: "pro",
name: "Pro",
price: "$79",
sub: "per month, billed annually",
cta: "Start Pro trial",
features: [
{ text: "Unlimited team members" },
{ text: "Unlimited projects" },
{ text: "100 GB storage" },
{ text: "Advanced analytics & reports" },
{ text: "Priority support 24/7" },
],
accent: BRAND,
glow: "rgba(99,102,241,0.22)",
highlighted: true,
},
{
id: "enterprise",
name: "Enterprise",
price: "$199",
sub: "per month, billed annually",
cta: "Contact sales",
features: [
{ text: "Everything in Pro" },
{ text: "Unlimited storage" },
{ text: "Custom SSO & SAML" },
{ text: "SLA 99.99% uptime" },
{ text: "Dedicated account manager" },
],
accent: BRAND_2,
glow: "rgba(139,92,246,0.14)",
highlighted: false,
},
];
// ── Helper: clamp interpolate ─────────────────────────────────────────
function lerp(
frame: number,
inputRange: [number, number],
outputRange: [number, number],
easing?: (t: number) => number
) {
return interpolate(frame, inputRange, outputRange, {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing,
});
}
// ── Checkmark icon ────────────────────────────────────────────────────
const Check: React.FC<{ color: string }> = ({ color }) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
style={{ flexShrink: 0, marginTop: 2 }}
>
<circle cx="8" cy="8" r="8" fill={color} fillOpacity={0.15} />
<path
d="M4.5 8.5L7 11L11.5 6"
stroke={color}
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
// ── "Most Popular" badge ──────────────────────────────────────────────
const PopularBadge: React.FC<{ frame: number; cardStartFrame: number; fps: number }> = ({
frame,
cardStartFrame,
fps,
}) => {
// Pulse every ~1.5s after card settles
const localFrame = Math.max(0, frame - cardStartFrame - 20);
const pulsePhase = (localFrame % 45) / 45;
const pulseScale = interpolate(
Math.sin(pulsePhase * Math.PI * 2),
[-1, 1],
[0.96, 1.04]
);
const glowOpacity = interpolate(
Math.sin(pulsePhase * Math.PI * 2),
[-1, 1],
[0.6, 1.0]
);
const badgeReveal = spring({
frame: Math.max(0, frame - cardStartFrame),
fps,
from: 0,
to: 1,
config: { damping: 14, stiffness: 180, mass: 0.6 },
});
return (
<div
style={{
position: "absolute",
top: -16,
left: "50%",
transform: `translateX(-50%) scale(${pulseScale * badgeReveal})`,
background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_2} 100%)`,
borderRadius: 20,
padding: "5px 18px",
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 12,
color: "#fff",
letterSpacing: "0.08em",
textTransform: "uppercase",
whiteSpace: "nowrap",
boxShadow: `0 0 20px rgba(99,102,241,${glowOpacity * 0.7})`,
zIndex: 10,
}}
>
Most Popular
</div>
);
};
// ── Feature row ───────────────────────────────────────────────────────
const FeatureRow: React.FC<{
text: string;
index: number;
cardStartFrame: number;
frame: number;
fps: number;
accent: string;
}> = ({ text, index, cardStartFrame, frame, fps, accent }) => {
// Each feature staggers in 12 frames after the previous
const featureDelay = cardStartFrame + 18 + index * 12;
const localFrame = Math.max(0, frame - featureDelay);
const progress = spring({
frame: localFrame,
fps,
from: 0,
to: 1,
config: { damping: 18, stiffness: 160, mass: 0.7 },
});
const opacity = lerp(localFrame, [0, 8], [0, 1], Easing.out(Easing.quad));
const translateX = interpolate(progress, [0, 1], [-16, 0]);
return (
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: 10,
opacity,
transform: `translateX(${translateX}px)`,
marginBottom: 10,
}}
>
<Check color={accent} />
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 14,
color: TEXT_MUTED,
lineHeight: 1.5,
}}
>
{text}
</span>
</div>
);
};
// ── Pricing card ──────────────────────────────────────────────────────
const CARD_ENTER_START = 30;
const CARD_STAGGER = 22; // frames between each card's entrance
const PricingCard: React.FC<{
plan: PricingPlan;
index: number;
frame: number;
fps: number;
durationInFrames: number;
}> = ({ plan, index, frame, fps, durationInFrames }) => {
const cardStartFrame = CARD_ENTER_START + index * CARD_STAGGER;
const localFrame = Math.max(0, frame - cardStartFrame);
// Card entrance: spring bounce from below
const slideProgress = spring({
frame: localFrame,
fps,
from: 0,
to: 1,
config: plan.highlighted
? { damping: 13, stiffness: 130, mass: 0.85 }
: { damping: 16, stiffness: 120, mass: 0.8 },
});
const translateY = interpolate(slideProgress, [0, 1], [80, 0]);
const cardOpacity = lerp(localFrame, [0, 10], [0, 1], Easing.out(Easing.quad));
// Pro card scale-pop: at frame 170 (after all cards are in)
const scalePop =
plan.highlighted
? spring({
frame: Math.max(0, frame - 170),
fps,
from: 1,
to: 1,
config: { damping: 10, stiffness: 250, mass: 0.5 },
})
: 1;
// Override: use a specific pop animation for the Pro card
const proPopFrame = Math.max(0, frame - 175);
const proScale = plan.highlighted
? spring({
frame: proPopFrame,
fps,
from: proPopFrame < 2 ? 1 : undefined,
to: 1,
config: { damping: 9, stiffness: 280, mass: 0.45 },
})
: 1;
// Subtle initial pop at frame 175 (after all cards entered)
const popTrigger = Math.max(0, frame - 175);
const popScale = plan.highlighted
? interpolate(
spring({
frame: popTrigger,
fps,
from: 0,
to: 1,
config: { damping: 8, stiffness: 300, mass: 0.4 },
}),
[0, 1],
[1, 1.035],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
)
: 1;
// Glow pulse for Pro card
const glowPulse = plan.highlighted
? lerp(
Math.sin(((frame - cardStartFrame) / 90) * Math.PI * 2),
[-1, 1],
[0.7, 1.0]
)
: 1;
const cardWidth = plan.highlighted ? 310 : 278;
const cardHeight = plan.highlighted ? 440 : 410;
return (
<div
style={{
position: "relative",
opacity: cardOpacity,
transform: `translateY(${translateY}px) scale(${popScale})`,
width: cardWidth,
height: cardHeight,
backgroundColor: plan.highlighted ? CARD_BG : SURFACE,
borderRadius: 20,
border: plan.highlighted
? `1.5px solid ${BRAND}`
: "1px solid rgba(255,255,255,0.06)",
boxShadow: plan.highlighted
? `0 0 60px rgba(99,102,241,${0.28 * glowPulse}), 0 8px 32px rgba(0,0,0,0.5)`
: "0 4px 20px rgba(0,0,0,0.3)",
display: "flex",
flexDirection: "column",
padding: "32px 28px 28px",
overflow: "visible",
}}
>
{/* Glow blob */}
<div
style={{
position: "absolute",
top: -60,
left: "50%",
transform: "translateX(-50%)",
width: 260,
height: 180,
borderRadius: "50%",
background: `radial-gradient(circle, ${plan.glow} 0%, transparent 70%)`,
filter: "blur(30px)",
opacity: glowPulse,
pointerEvents: "none",
zIndex: 0,
}}
/>
{/* Most popular badge */}
{plan.highlighted && (
<PopularBadge
frame={frame}
cardStartFrame={cardStartFrame}
fps={fps}
/>
)}
{/* Plan name */}
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 18,
color: plan.accent,
letterSpacing: "0.04em",
textTransform: "uppercase",
position: "relative",
zIndex: 1,
}}
>
{plan.name}
</div>
{/* Price */}
<div
style={{
display: "flex",
alignItems: "baseline",
gap: 4,
marginTop: 16,
position: "relative",
zIndex: 1,
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
fontSize: plan.highlighted ? 56 : 48,
color: TEXT,
letterSpacing: "-0.04em",
lineHeight: 1,
}}
>
{plan.price}
</span>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 14,
color: TEXT_MUTED,
lineHeight: 1.4,
maxWidth: 70,
}}
>
/mo
</span>
</div>
{/* Billed annually */}
<div
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 12,
color: TEXT_MUTED,
marginTop: 6,
position: "relative",
zIndex: 1,
}}
>
{plan.sub}
</div>
{/* Divider */}
<div
style={{
height: 1,
backgroundColor: "rgba(255,255,255,0.07)",
margin: "20px 0",
position: "relative",
zIndex: 1,
}}
/>
{/* Features */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
position: "relative",
zIndex: 1,
}}
>
{plan.features.map((f, i) => (
<FeatureRow
key={f.text}
text={f.text}
index={i}
cardStartFrame={cardStartFrame}
frame={frame}
fps={fps}
accent={plan.accent}
/>
))}
</div>
{/* CTA button */}
<CtaButton
label={plan.cta}
accent={plan.accent}
highlighted={plan.highlighted}
cardStartFrame={cardStartFrame}
frame={frame}
fps={fps}
/>
</div>
);
};
// ── CTA button ────────────────────────────────────────────────────────
const CtaButton: React.FC<{
label: string;
accent: string;
highlighted: boolean;
cardStartFrame: number;
frame: number;
fps: number;
}> = ({ label, accent, highlighted, cardStartFrame, frame, fps }) => {
// Button fades in after features are done
const btnDelay = cardStartFrame + 80;
const localFrame = Math.max(0, frame - btnDelay);
const progress = spring({
frame: localFrame,
fps,
from: 0,
to: 1,
config: { damping: 18, stiffness: 140, mass: 0.7 },
});
const opacity = lerp(localFrame, [0, 10], [0, 1], Easing.out(Easing.quad));
const translateY = interpolate(progress, [0, 1], [12, 0]);
return (
<div
style={{
opacity,
transform: `translateY(${translateY}px)`,
marginTop: 20,
borderRadius: 12,
background: highlighted
? `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_2} 100%)`
: "rgba(255,255,255,0.06)",
border: highlighted ? "none" : `1px solid rgba(255,255,255,0.1)`,
padding: "13px 0",
textAlign: "center",
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 14,
color: highlighted ? "#fff" : TEXT_MUTED,
letterSpacing: "0.02em",
position: "relative",
zIndex: 1,
boxShadow: highlighted
? `0 4px 20px rgba(99,102,241,0.35)`
: "none",
}}
>
{label}
</div>
);
};
// ── Header ────────────────────────────────────────────────────────────
const Header: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
const titleProgress = spring({
frame,
fps,
from: 0,
to: 1,
config: { damping: 18, stiffness: 100, mass: 0.9 },
});
const titleY = interpolate(titleProgress, [0, 1], [-20, 0]);
const titleOpacity = lerp(frame, [0, 15], [0, 1], Easing.out(Easing.quad));
const subOpacity = lerp(frame, [10, 28], [0, 1], Easing.out(Easing.quad));
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
marginBottom: 44,
}}
>
{/* Eyebrow */}
<div
style={{
opacity: subOpacity,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 12,
color: BRAND,
letterSpacing: "0.15em",
textTransform: "uppercase",
marginBottom: 10,
}}
>
Flowbase — Simple Pricing
</div>
{/* Main heading */}
<div
style={{
opacity: titleOpacity,
transform: `translateY(${titleY}px)`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
fontSize: 42,
color: TEXT,
letterSpacing: "-0.025em",
lineHeight: 1,
textAlign: "center",
}}
>
Choose your plan
</div>
{/* Subtitle */}
<div
style={{
opacity: subOpacity,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
fontSize: 16,
color: TEXT_MUTED,
marginTop: 12,
textAlign: "center",
}}
>
Scale as you grow. Upgrade or downgrade at any time.
</div>
</div>
);
};
// ── Footer tagline ────────────────────────────────────────────────────
const FooterTagline: React.FC<{ frame: number }> = ({ frame }) => {
// Appears after all cards + features are done, around frame 240
const opacity = lerp(frame, [238, 260], [0, 1], Easing.out(Easing.cubic));
const translateY = lerp(frame, [238, 260], [12, 0], Easing.out(Easing.cubic));
return (
<div
style={{
opacity,
transform: `translateY(${translateY}px)`,
marginTop: 36,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 14,
color: TEXT_MUTED,
textAlign: "center",
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="7.5" stroke={SUCCESS} strokeOpacity={0.6} />
<path
d="M5 8.5L7 10.5L11 6.5"
stroke={SUCCESS}
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
14-day free trial — no credit card required
</div>
);
};
// ── Background decoration ─────────────────────────────────────────────
const Background: React.FC<{ frame: number }> = ({ frame }) => {
const pulse = interpolate(
Math.sin((frame / 180) * Math.PI * 2),
[-1, 1],
[0.7, 1.0]
);
return (
<>
{/* Dot grid */}
<div
style={{
position: "absolute",
inset: 0,
backgroundImage:
"radial-gradient(rgba(255,255,255,0.06) 1px, transparent 1px)",
backgroundSize: "32px 32px",
pointerEvents: "none",
}}
/>
{/* Top-center hero glow */}
<div
style={{
position: "absolute",
top: -160,
left: "50%",
transform: "translateX(-50%)",
width: 700,
height: 400,
borderRadius: "50%",
background: `radial-gradient(ellipse, rgba(99,102,241,0.1) 0%, transparent 70%)`,
filter: "blur(60px)",
opacity: pulse,
pointerEvents: "none",
}}
/>
{/* Bottom-right accent */}
<div
style={{
position: "absolute",
bottom: -80,
right: -80,
width: 360,
height: 360,
borderRadius: "50%",
background: `radial-gradient(circle, rgba(6,182,212,0.07) 0%, transparent 70%)`,
filter: "blur(50px)",
opacity: pulse * 0.8,
pointerEvents: "none",
}}
/>
</>
);
};
// ── Main composition ──────────────────────────────────────────────────
export const PricingReveal: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Global fade out in last 0.5s (15 frames)
const globalOpacity = interpolate(
frame,
[durationInFrames - 15, durationInFrames],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
backgroundColor: BG,
opacity: globalOpacity,
overflow: "hidden",
}}
>
<Background frame={frame} />
{/* Layout */}
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "40px 48px 32px",
}}
>
<Header frame={frame} fps={fps} />
{/* Cards row */}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 18,
}}
>
{PLANS.map((plan, i) => (
<PricingCard
key={plan.id}
plan={plan}
index={i}
frame={frame}
fps={fps}
durationInFrames={durationInFrames}
/>
))}
</div>
<FooterTagline frame={frame} />
</div>
</AbsoluteFill>
);
};
// ── Remotion root ─────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="PricingReveal"
component={PricingReveal}
durationInFrames={DURATION}
fps={30}
width={1280}
height={720}
/>
);Pricing Reveal Animation
A 10-second Remotion composition that animates a three-tier SaaS pricing scene on a dark cinematic background. The sequence opens with the “Flowbase — Simple Pricing” header sliding down into frame, then each card (Starter, Pro, Enterprise) launches upward in a staggered spring sequence 22 frames apart. The center Pro card is visually elevated — slightly taller and wider, framed by a persistent indigo glow and a “Most Popular” badge that pulses with a breathing scale animation throughout the composition.
Inside each card, four or five feature rows enter from the left one by one using individual spring curves, landing in a clean staggered cascade. Once all features are revealed, the CTA button rises from below and fades in. At frame 175, after all three cards have fully entered, the Pro card fires a quick scale-pop to draw the viewer’s eye back to the recommended tier. The composition closes around frame 240 with a “14-day free trial — no credit card required” tagline fading up from below, then everything gently fades to black over the final 15 frames.
The layout is pure React with inline styles only — no external CSS, no fonts, no assets. A subtle dot-grid overlay and two large radial blurs (indigo overhead, cyan bottom-right) give depth to the dark background without competing with the card content.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 10 s (300 frames) |
Timeline
| Time | Frame | Event |
|---|---|---|
| 0:00 | 0 | Header eyebrow + title slides in |
| 0:07 | 1 | Subtitle fades in |
| 0:01 | 30 | Starter card springs up |
| 0:02 | 52 | Pro card springs up (with badge pop) |
| 0:02.5 | 74 | Enterprise card springs up |
| 0:01–0:04 | 48–154 | Feature rows stagger in per card |
| 0:03–0:05 | 110–175 | CTA buttons reveal per card |
| 0:05.8 | 175 | Pro card scale-pop emphasis |
| 0:08 | 238 | ”14-day free trial” tagline fades in |
| 0:09.5 | 285 | Global fade-out begins |
| 0:10 | 300 | End |