Discount Banner (Remotion)
A polished 3-second wide-format discount banner rendered in Remotion at 1280x720 30fps — features a bold gradient ribbon (violet #7c3aed to pink #db2777), a large '15% OFF' badge that spring-rotates 0 to 360 degrees and settles with a bounce, a scrolling RTL ticker strip reading 'FREE SHIPPING on orders over $50' driven by interpolate translateX, the 'Luxe Store' brand lockup with an animated logo mark, and a shop-link with an expanding underline reveal — all fading to black in the final 20 frames.
Preview
Code
import React from "react";
import {
AbsoluteFill,
Composition,
interpolate,
spring,
useCurrentFrame,
useVideoConfig,
Easing,
} from "remotion";
// ── Config constants (customize here) ─────────────────────────────────────────
const BRAND_NAME = "Luxe Store";
const BRAND_INITIALS = "LS";
const DISCOUNT_LABEL = "15% OFF";
const BADGE_SUB = "Your entire order";
const CTA_TEXT = "SHOP NOW";
const TICKER_TEXT =
"FREE SHIPPING on orders over $50 • USE CODE: LUXE15 • LIMITED TIME OFFER • FREE SHIPPING on orders over $50 • USE CODE: LUXE15 • LIMITED TIME OFFER • ";
// Palette
const VIOLET = "#7c3aed";
const VIOLET_LIGHT = "#a78bfa";
const PINK = "#db2777";
const PINK_LIGHT = "#f472b6";
const BG = "#0a0a0f";
const SURFACE = "rgba(255,255,255,0.07)";
const BORDER = "rgba(255,255,255,0.12)";
// Layout — banner sits in a 1280×200 strip centered in 1280×720
const BANNER_HEIGHT = 210;
const BANNER_Y = (720 - BANNER_HEIGHT) / 2; // 255
// ── BackgroundLayer — gradient ribbon + radial glows ──────────────────────────
const BackgroundLayer: React.FC<{ frame: number }> = ({ frame }) => {
const bannerOpacity = interpolate(frame, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
const violetGlowOpacity = interpolate(frame, [8, 35], [0, 0.7], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
const pinkGlowOpacity = interpolate(frame, [12, 40], [0, 0.55], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Slow ambient shimmer drift
const shimmerX = interpolate(frame, [0, 90], [-200, 1480], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.inOut(Easing.quad),
});
return (
<>
{/* Full scene dark bg */}
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: BG,
}}
/>
{/* Gradient banner ribbon */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: BANNER_Y,
height: BANNER_HEIGHT,
background: `linear-gradient(105deg, ${VIOLET} 0%, #9d174d 50%, ${PINK} 100%)`,
opacity: bannerOpacity,
overflow: "hidden",
}}
>
{/* Diagonal highlight stripe */}
<div
style={{
position: "absolute",
inset: 0,
background:
"linear-gradient(105deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.0) 60%)",
}}
/>
{/* Moving shimmer sweep */}
<div
style={{
position: "absolute",
top: 0,
left: shimmerX,
width: 180,
height: "100%",
background:
"linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.13) 50%, transparent 100%)",
transform: "skewX(-20deg)",
}}
/>
{/* Top edge highlight */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 1,
background: "rgba(255,255,255,0.25)",
}}
/>
{/* Bottom edge shadow */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 1,
background: "rgba(0,0,0,0.4)",
}}
/>
</div>
{/* Violet radial glow behind left section */}
<div
style={{
position: "absolute",
left: -60,
top: BANNER_Y - 140,
width: 700,
height: 490,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${VIOLET}66 0%, transparent 65%)`,
opacity: violetGlowOpacity,
pointerEvents: "none",
}}
/>
{/* Pink radial glow behind badge center */}
<div
style={{
position: "absolute",
left: "40%",
top: BANNER_Y - 80,
width: 600,
height: 380,
borderRadius: "50%",
background: `radial-gradient(ellipse, ${PINK}44 0%, transparent 65%)`,
transform: "translateX(-50%)",
opacity: pinkGlowOpacity,
pointerEvents: "none",
}}
/>
</>
);
};
// ── BrandLockup — logo mark + wordmark ────────────────────────────────────────
const BrandLockup: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
// Logo mark springs in from left
const logoX = spring({
frame,
fps,
from: -60,
to: 0,
config: { damping: 14, stiffness: 130, mass: 0.75 },
});
const logoOpacity = interpolate(frame, [0, 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Wordmark fades + rises slightly after logo
const f2 = Math.max(0, frame - 10);
const wordY = spring({
frame: f2,
fps,
from: 18,
to: 0,
config: { damping: 16, stiffness: 120, mass: 0.8 },
});
const wordOpacity = interpolate(f2, [0, 18], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Tagline even more delayed
const f3 = Math.max(0, frame - 20);
const tagOpacity = interpolate(f3, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 18,
}}
>
{/* Logo mark */}
<div
style={{
opacity: logoOpacity,
transform: `translateX(${logoX}px)`,
flexShrink: 0,
}}
>
<div
style={{
width: 58,
height: 58,
borderRadius: 14,
background: "rgba(255,255,255,0.18)",
border: "1.5px solid rgba(255,255,255,0.35)",
display: "flex",
alignItems: "center",
justifyContent: "center",
boxShadow: `0 4px 20px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.2)`,
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 20,
color: "#ffffff",
letterSpacing: -0.5,
}}
>
{BRAND_INITIALS}
</span>
</div>
</div>
{/* Text column */}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<div
style={{
opacity: wordOpacity,
transform: `translateY(${wordY}px)`,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
fontSize: 28,
color: "#ffffff",
letterSpacing: -0.5,
lineHeight: 1,
textShadow: "0 1px 8px rgba(0,0,0,0.4)",
}}
>
{BRAND_NAME}
</div>
<div
style={{
opacity: tagOpacity * 0.65,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 500,
fontSize: 12,
color: "rgba(255,255,255,0.75)",
letterSpacing: 2.5,
textTransform: "uppercase",
}}
>
Premium Fashion
</div>
</div>
</div>
);
};
// ── DiscountBadge — rotating 15% OFF medallion ────────────────────────────────
const DiscountBadge: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
// Scale spring — pops in
const scaleIn = spring({
frame,
fps,
from: 0,
to: 1,
config: { damping: 9, stiffness: 100, mass: 0.6 },
});
// Full rotation spring: 0 → 360 (over-rotates then settles)
const rotation = spring({
frame,
fps,
from: 0,
to: 360,
config: { damping: 14, stiffness: 70, mass: 1.1 },
});
const badgeOpacity = interpolate(frame, [0, 8], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Sub-label fades in after badge lands (around frame 38)
const f2 = Math.max(0, frame - 40);
const subOpacity = interpolate(f2, [0, 14], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Subtle continuous pulse after settling
const settled = frame > 45 ? 1 : 0;
const pulse = settled * (1 + Math.sin((frame / 22) * Math.PI) * 0.018);
return (
<div
style={{
opacity: badgeOpacity,
transform: `scale(${scaleIn * pulse}) rotate(${rotation}deg)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 0,
}}
>
{/* Outer ring */}
<div
style={{
position: "relative",
width: 148,
height: 148,
borderRadius: "50%",
background: `conic-gradient(from 0deg, ${VIOLET_LIGHT}, ${PINK_LIGHT}, ${VIOLET_LIGHT})`,
padding: 3,
boxShadow: `0 0 40px ${VIOLET}99, 0 0 80px ${PINK}55`,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{/* Inner disk */}
<div
style={{
width: "100%",
height: "100%",
borderRadius: "50%",
background: `radial-gradient(circle at 40% 35%, #2d1b69 0%, #1a0533 60%, #0f0022 100%)`,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 0,
}}
>
{/* Discount number */}
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 900,
fontSize: 42,
color: "#ffffff",
letterSpacing: -2,
lineHeight: 1,
textShadow: `0 0 20px ${VIOLET_LIGHT}cc`,
// Counter-rotate so text stays upright as badge spins
transform: `rotate(${-rotation}deg)`,
}}
>
{DISCOUNT_LABEL}
</span>
</div>
</div>
{/* Sub-label pill — counter-rotated to stay readable */}
<div
style={{
marginTop: -14,
opacity: subOpacity,
transform: `rotate(${-rotation}deg)`,
}}
>
<div
style={{
background: "rgba(255,255,255,0.15)",
border: "1px solid rgba(255,255,255,0.22)",
borderRadius: 20,
padding: "4px 14px",
backdropFilter: "blur(8px)",
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 11,
color: "rgba(255,255,255,0.9)",
letterSpacing: 1.5,
textTransform: "uppercase",
}}
>
{BADGE_SUB}
</span>
</div>
</div>
</div>
);
};
// ── CTAButton — SHOP NOW with animated underline ──────────────────────────────
const CTAButton: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
// Whole block springs in from right
const f0 = Math.max(0, frame - 22);
const ctaX = spring({
frame: f0,
fps,
from: 70,
to: 0,
config: { damping: 15, stiffness: 120, mass: 0.8 },
});
const ctaOpacity = interpolate(f0, [0, 16], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Underline sweeps left → right starting at frame 45
const fUnder = Math.max(0, frame - 45);
const underlineWidth = interpolate(fUnder, [0, 25], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Arrow nudge on loop after frame 50
const arrowX =
frame > 50 ? 4 * Math.sin(((frame - 50) / 18) * Math.PI) : 0;
return (
<div
style={{
opacity: ctaOpacity,
transform: `translateX(${ctaX}px)`,
display: "flex",
flexDirection: "column",
alignItems: "flex-end",
gap: 8,
}}
>
{/* Pill button */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
background: "rgba(255,255,255,0.14)",
border: "1.5px solid rgba(255,255,255,0.28)",
borderRadius: 50,
padding: "10px 26px",
boxShadow: "0 4px 20px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.1)",
}}
>
<span
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
fontSize: 16,
color: "#ffffff",
letterSpacing: 2.5,
textTransform: "uppercase",
}}
>
{CTA_TEXT}
</span>
{/* Arrow */}
<span
style={{
fontSize: 16,
transform: `translateX(${arrowX}px)`,
display: "inline-block",
}}
>
→
</span>
</div>
{/* Animated underline bar */}
<div
style={{
width: 200,
height: 2,
borderRadius: 1,
backgroundColor: "rgba(255,255,255,0.12)",
overflow: "hidden",
position: "relative",
}}
>
<div
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: `${underlineWidth}%`,
background: `linear-gradient(90deg, ${VIOLET_LIGHT}, ${PINK_LIGHT})`,
borderRadius: 1,
boxShadow: `0 0 8px ${VIOLET_LIGHT}88`,
}}
/>
</div>
</div>
);
};
// ── TickerStrip — RTL scrolling text at bottom of banner ──────────────────────
const TickerStrip: React.FC<{ frame: number }> = ({ frame }) => {
const stripOpacity = interpolate(frame, [28, 46], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
// RTL scroll: translateX from 0 → -960 (one full scroll cycle over 90 frames)
// We loop by using modulo-like interpolation — text is duplicated in the string
const tickerX = interpolate(frame, [0, 90], [0, -960], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.linear,
});
return (
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 38,
backgroundColor: "rgba(0,0,0,0.28)",
borderTop: "1px solid rgba(255,255,255,0.1)",
overflow: "hidden",
display: "flex",
alignItems: "center",
opacity: stripOpacity,
}}
>
{/* Scrolling text — wide enough to loop seamlessly */}
<div
style={{
transform: `translateX(${tickerX}px)`,
whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
gap: 0,
}}
>
{/* Render 3 copies to ensure seamless loop */}
{[0, 1, 2].map((i) => (
<span
key={i}
style={{
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 600,
fontSize: 12,
color: "rgba(255,255,255,0.82)",
letterSpacing: 1.8,
textTransform: "uppercase",
paddingRight: 40,
}}
>
{TICKER_TEXT}
</span>
))}
</div>
{/* Left fade edge */}
<div
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: 80,
background: `linear-gradient(90deg, rgba(100,20,80,0.8), transparent)`,
pointerEvents: "none",
}}
/>
{/* Right fade edge */}
<div
style={{
position: "absolute",
right: 0,
top: 0,
bottom: 0,
width: 80,
background: `linear-gradient(270deg, rgba(100,20,80,0.8), transparent)`,
pointerEvents: "none",
}}
/>
</div>
);
};
// ── Divider — vertical separator between brand and badge ─────────────────────
const VerticalDivider: React.FC<{ frame: number }> = ({ frame }) => {
const height = interpolate(frame, [18, 42], [0, 100], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
return (
<div
style={{
width: 1,
height: 100,
position: "relative",
overflow: "hidden",
flexShrink: 0,
}}
>
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: `${height}%`,
background:
"linear-gradient(180deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)",
}}
/>
</div>
);
};
// ── Main Composition ───────────────────────────────────────────────────────────
export const DiscountBanner: React.FC = () => {
const frame = useCurrentFrame();
const { fps, durationInFrames } = useVideoConfig();
// Global fade-out in last 20 frames
const globalOpacity = interpolate(
frame,
[durationInFrames - 20, durationInFrames],
[1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<AbsoluteFill
style={{
backgroundColor: BG,
opacity: globalOpacity,
overflow: "hidden",
}}
>
{/* Layer 0: Background ribbon + glows */}
<BackgroundLayer frame={frame} />
{/* Layer 1: Main banner content strip */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: BANNER_Y,
height: BANNER_HEIGHT,
display: "flex",
alignItems: "center",
paddingLeft: 48,
paddingRight: 48,
overflow: "hidden",
}}
>
{/* LEFT: Brand lockup */}
<div
style={{
flex: "0 0 auto",
display: "flex",
alignItems: "center",
}}
>
<BrandLockup frame={frame} fps={fps} />
</div>
{/* Divider */}
<div style={{ marginLeft: 32, marginRight: 32 }}>
<VerticalDivider frame={frame} />
</div>
{/* CENTER: Discount badge */}
<div
style={{
flex: 1,
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<DiscountBadge frame={frame} fps={fps} />
</div>
{/* RIGHT: CTA */}
<div style={{ flex: "0 0 auto" }}>
<CTAButton frame={frame} fps={fps} />
</div>
</div>
{/* Layer 2: Ticker strip at bottom of banner */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: BANNER_Y,
height: BANNER_HEIGHT,
}}
>
<TickerStrip frame={frame} />
</div>
{/* Layer 3: Edge vignette on full scene */}
<div
style={{
position: "absolute",
inset: 0,
background:
"radial-gradient(ellipse at 50% 50%, transparent 45%, rgba(0,0,0,0.5) 100%)",
pointerEvents: "none",
}}
/>
</AbsoluteFill>
);
};
// ── Remotion Root ──────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="DiscountBanner"
component={DiscountBanner}
durationInFrames={90}
fps={30}
width={1280}
height={720}
/>
);Discount Banner
A cinematic 3-second promotional banner built with Remotion. The scene opens on a rich violet-to-pink gradient ribbon spanning the full width, with radial glows blooming behind the badge and ticker areas. At the center of the banner a large “15% OFF” badge rotates a full 360° via a spring animation — over-shooting slightly before settling — then sits locked in place for the remainder of the clip, commanding the viewer’s eye with a glowing drop-shadow.
The brand section occupies the left column: an animated logo mark (a rounded-square with initials) springs in from the left, followed by the “Luxe Store” wordmark fading up. The right column holds the call-to-action: a “SHOP NOW” link text where an underline bar sweeps across from left to right via interpolate, reinforcing the clickable intent. A continuous scrolling ticker strip at the bottom of the banner moves from right to left using interpolate on translateX, looping the message “FREE SHIPPING on orders over $50 • USE CODE: LUXE15 •” across the full banner width. The final 20 frames fade the entire scene to black via globalOpacity.
All text constants (DISCOUNT_LABEL, BRAND_NAME, TICKER_TEXT, CTA_TEXT, BADGE_SUB, VIOLET, PINK) are declared at the top of the file for zero-friction customization — swap colors, copy, and discount value without touching component logic.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1280 × 720 |
| FPS | 30 |
| Duration | 3 s (90 frames) |
Timeline
| Time | Action |
|---|---|
| 0 – 0.4 s (frames 0–12) | Background gradient ribbon fades in; radial glows bloom |
| 0.4 – 1.0 s (frames 12–30) | Logo mark springs in from left; brand name fades up; “15% OFF” badge begins rotation |
| 1.0 – 1.5 s (frames 30–45) | Badge rotation completes and settles; ticker strip begins scrolling RTL |
| 1.5 – 2.3 s (frames 45–70) | CTA underline sweeps across; badge sub-label fades in; all elements held |
| 2.3 – 3.0 s (frames 70–90) | Global opacity fades to black |