Sale Announcement Spot (Remotion)
A punchy 3-second retail sale spot — the SALE badge explodes in with an overshoot spring scale, a 50% OFF discount slides in elastically from the left, original price fades with strikethrough, new price bounces down from above, and eight confetti dots in vivid red-orange and gold scatter upward with staggered float animations across a deep off-black canvas. High-energy and retail-ready.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Composition,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Easing,
} from "remotion";
// ── Config ────────────────────────────────────────────────────────────────────
const BRAND = "ShopNow";
const DISCOUNT_PCT = "50% OFF";
const ORIGINAL_PRICE = "$199.99";
const NEW_PRICE = "$99.99";
const ACCENT_COLOR = "#ef4444"; // vivid red
const GOLD_COLOR = "#f59e0b"; // warm gold for accents
const BG_COLOR = "#0a0a0f"; // near-black
const TEXT_WHITE = "#ffffff";
const TEXT_MUTED = "rgba(255,255,255,0.45)";
const CONFETTI_DOTS = [
{ x: 120, y: 580, color: "#ef4444", size: 14, delay: 0 },
{ x: 280, y: 640, color: "#f59e0b", size: 10, delay: 4 },
{ x: 460, y: 600, color: "#fbbf24", size: 16, delay: 2 },
{ x: 680, y: 660, color: "#ef4444", size: 12, delay: 6 },
{ x: 820, y: 590, color: "#f97316", size: 18, delay: 3 },
{ x: 980, y: 630, color: "#fbbf24", size: 10, delay: 7 },
{ x: 1100, y: 600, color: "#ef4444", size: 14, delay: 1 },
{ x: 1200, y: 650, color: "#f59e0b", size: 12, delay: 5 },
];
// ── Background layer ──────────────────────────────────────────────────────────
const Background: React.FC = () => (
<>
{/* Deep radial glow centred on canvas */}
<div
style={{
position: "absolute",
inset: 0,
background: `radial-gradient(ellipse 860px 500px at 50% 50%,
${ACCENT_COLOR}18 0%,
${GOLD_COLOR}0a 45%,
transparent 72%)`,
}}
/>
{/* Subtle vignette */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse 100% 100% at 50% 50%, transparent 55%, rgba(0,0,0,0.7) 100%)",
}}
/>
</>
);
// ── Confetti dots ─────────────────────────────────────────────────────────────
const ConfettiDot: React.FC<{
frame: number;
fps: number;
x: number;
y: number;
color: string;
size: number;
delay: number;
}> = ({ frame, fps, x, y, color, size, delay }) => {
const f = Math.max(0, frame - delay);
const opacity = interpolate(f, [0, 10, 68, 90], [0, 0.9, 0.75, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const floatY = interpolate(
f,
[0, 90 - delay],
[0, -110],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
}
);
const wobbleX = Math.sin((f / 14) * Math.PI) * 12;
const scale = spring({
frame: f,
fps,
from: 0,
to: 1,
config: { damping: 10, stiffness: 200, mass: 0.5 },
});
return (
<div
style={{
position: "absolute",
left: x,
top: y,
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
boxShadow: `0 0 ${size * 1.5}px ${color}88`,
opacity,
transform: `translate(${wobbleX}px, ${floatY}px) scale(${scale})`,
}}
/>
);
};
const ConfettiLayer: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => (
<>
{CONFETTI_DOTS.map((dot, i) => (
<ConfettiDot key={i} frame={frame} fps={fps} {...dot} />
))}
</>
);
// ── Brand badge (top) ─────────────────────────────────────────────────────────
const BrandBadge: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => {
const opacity = interpolate(frame, [0, 12], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const translateY = spring({
frame,
fps,
from: -24,
to: 0,
config: { damping: 18, stiffness: 140 },
});
return (
<div
style={{
opacity,
transform: `translateY(${translateY}px)`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 700,
fontSize: 18,
letterSpacing: 5,
textTransform: "uppercase" as const,
color: TEXT_MUTED,
marginBottom: 24,
}}
>
{BRAND}
</div>
);
};
// ── SALE title ────────────────────────────────────────────────────────────────
const SaleTitle: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => {
// Overshoots to 1.2 then settles at 1
const scale = spring({
frame,
fps,
from: 0,
to: 1,
config: { damping: 8, stiffness: 220, mass: 0.7 },
});
const opacity = interpolate(frame, [0, 8], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Subtle hue-pulse glow after entry
const glowIntensity = interpolate(
frame,
[18, 35, 52, 70],
[0.3, 1, 0.4, 0.9],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<div
style={{
opacity,
transform: `scale(${scale})`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 148,
color: TEXT_WHITE,
letterSpacing: -6,
lineHeight: 0.9,
textShadow: `0 0 60px ${ACCENT_COLOR}${Math.round(glowIntensity * 255)
.toString(16)
.padStart(2, "0")}`,
}}
>
SALE
</div>
);
};
// ── Discount badge ────────────────────────────────────────────────────────────
const DiscountBadge: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => {
const delayed = Math.max(0, frame - 15);
// Elastic slide from left
const translateX = spring({
frame: delayed,
fps,
from: -420,
to: 0,
config: { damping: 10, stiffness: 160, mass: 0.8 },
});
const opacity = interpolate(delayed, [0, 8], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
opacity,
transform: `translateX(${translateX}px)`,
marginTop: 18,
display: "flex",
alignItems: "center",
gap: 0,
}}
>
{/* Left accent line */}
<div
style={{
width: 6,
height: 54,
backgroundColor: ACCENT_COLOR,
borderRadius: "4px 0 0 4px",
marginRight: 16,
boxShadow: `0 0 16px ${ACCENT_COLOR}`,
}}
/>
<div
style={{
backgroundColor: ACCENT_COLOR,
borderRadius: 12,
padding: "10px 30px",
boxShadow: `0 8px 40px ${ACCENT_COLOR}55, 0 2px 12px ${ACCENT_COLOR}44`,
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 42,
color: TEXT_WHITE,
letterSpacing: -1,
}}
>
{DISCOUNT_PCT}
</span>
</div>
</div>
);
};
// ── Pricing row ───────────────────────────────────────────────────────────────
const PricingRow: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => {
// Original price — fade in with strikethrough
const origDelay = Math.max(0, frame - 24);
const origOpacity = interpolate(origDelay, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// New price — bounces in from above
const newDelay = Math.max(0, frame - 30);
const newY = spring({
frame: newDelay,
fps,
from: -50,
to: 0,
config: { damping: 10, stiffness: 200, mass: 0.6 },
});
const newOpacity = interpolate(newDelay, [0, 10], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
marginTop: 28,
display: "flex",
alignItems: "center",
gap: 28,
}}
>
{/* Original price with strikethrough */}
<div
style={{
opacity: origOpacity,
position: "relative",
display: "inline-block",
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 32,
color: TEXT_MUTED,
letterSpacing: -0.5,
}}
>
{ORIGINAL_PRICE}
</span>
{/* Strikethrough overlay */}
<div
style={{
position: "absolute",
top: "50%",
left: -4,
right: -4,
height: 3,
backgroundColor: ACCENT_COLOR,
borderRadius: 2,
transform: "translateY(-50%) rotate(-6deg)",
boxShadow: `0 0 8px ${ACCENT_COLOR}`,
}}
/>
</div>
{/* Arrow separator */}
<div
style={{
opacity: newOpacity,
fontFamily: "system-ui, -apple-system, sans-serif",
fontSize: 28,
color: GOLD_COLOR,
fontWeight: 700,
}}
>
→
</div>
{/* New price */}
<div
style={{
opacity: newOpacity,
transform: `translateY(${newY}px)`,
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 52,
color: GOLD_COLOR,
letterSpacing: -1.5,
textShadow: `0 0 30px ${GOLD_COLOR}88`,
}}
>
{NEW_PRICE}
</span>
</div>
</div>
);
};
// ── Urgency tagline ───────────────────────────────────────────────────────────
const UrgencyTag: React.FC<{ frame: number }> = ({ frame }) => {
const delayed = Math.max(0, frame - 44);
const opacity = interpolate(delayed, [0, 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const translateY = interpolate(delayed, [0, 16], [12, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Pulse the dot
const dotPulse = 0.65 + 0.35 * Math.sin((frame / 12) * Math.PI);
return (
<div
style={{
opacity,
transform: `translateY(${translateY}px)`,
marginTop: 32,
display: "flex",
alignItems: "center",
gap: 10,
}}
>
<div
style={{
width: 10,
height: 10,
borderRadius: "50%",
backgroundColor: ACCENT_COLOR,
boxShadow: `0 0 ${10 * dotPulse}px ${ACCENT_COLOR}`,
opacity: dotPulse,
}}
/>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 16,
letterSpacing: 3,
textTransform: "uppercase" as const,
color: TEXT_MUTED,
}}
>
Limited time offer
</span>
</div>
);
};
// ── Decorative corner rings ───────────────────────────────────────────────────
const CornerRings: React.FC<{ frame: number; fps: number }> = ({
frame,
fps,
}) => {
const scale = spring({
frame,
fps,
from: 0.4,
to: 1,
config: { damping: 20, stiffness: 80 },
});
const opacity = interpolate(frame, [0, 20], [0, 0.18], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const ringStyle = (top: number, left: number): React.CSSProperties => ({
position: "absolute",
top,
left,
width: 200,
height: 200,
borderRadius: "50%",
border: `2px solid ${ACCENT_COLOR}`,
opacity,
transform: `scale(${scale})`,
});
return (
<>
<div style={ringStyle(-60, -60)} />
<div
style={{
...ringStyle(-60, -60),
width: 140,
height: 140,
top: -30,
left: -30,
opacity: opacity * 1.5,
}}
/>
<div style={ringStyle(580, 1140)} />
<div
style={{
...ringStyle(580, 1140),
width: 140,
height: 140,
top: 610,
left: 1170,
opacity: opacity * 1.5,
}}
/>
</>
);
};
// ── Main composition ──────────────────────────────────────────────────────────
export const SaleAnnouncementSpot: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
const globalOpacity = interpolate(
frame,
[durationInFrames - 20, durationInFrames],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
backgroundColor: BG_COLOR,
opacity: globalOpacity,
overflow: "hidden",
}}
>
<Background />
<CornerRings frame={frame} fps={fps} />
<ConfettiLayer frame={frame} fps={fps} />
{/* Centre stack */}
<AbsoluteFill
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<BrandBadge frame={frame} fps={fps} />
<SaleTitle frame={frame} fps={fps} />
<DiscountBadge frame={frame} fps={fps} />
<PricingRow frame={frame} fps={fps} />
<UrgencyTag frame={frame} />
</AbsoluteFill>
</AbsoluteFill>
);
};
// ── Remotion root ─────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="SaleAnnouncementSpot"
component={SaleAnnouncementSpot}
durationInFrames={90}
fps={30}
width={1280}
height={720}
/>
);Sale Announcement Spot
A high-energy 3-second sale announcement animation built for product drops, flash sales, and promotional reels. The composition layers a radial glow background with a cinematic depth stack: confetti particles scatter behind the central badge, while the pricing elements arrive sequentially to drive the viewer’s eye from discount percentage down to the new price.
The SALE title explodes in with a spring that overshoots to 1.2× before settling at 1×, creating an instant visual punch. The 50% OFF badge slides in from the left edge with an elastic spring, followed by a fading original price with a red strikethrough, and finally the new price bouncing down from above. All elements are wrapped in a global fade-out over the last 20 frames for a clean exit.
Key constants are all defined at the top of the file — swap BRAND, DISCOUNT_PCT, ORIGINAL_PRICE, NEW_PRICE, and ACCENT_COLOR to adapt this spot to any campaign in seconds.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 3 s (90 frames) |
Timeline
| Time | Action |
|---|---|
| 0–0.4 s (0–12 f) | Confetti dots begin staggered float-up from random positions |
| 0–0.5 s (0–15 f) | SALE badge scales in with overshoot spring (0 → 1.2 → 1) |
| 0.5–0.9 s (15–27 f) | 50% OFF discount badge slides in elastically from the left |
| 0.8–1.2 s (24–36 f) | Original price strikethrough fades in |
| 1.0–1.5 s (30–45 f) | New price bounces down with spring from above |
| 2.3–3.0 s (70–90 f) | Global fade-out to black |