Swipe-Up CTA Animation (Remotion)
A looping swipe-up call-to-action animation for Stories and Reels — animated hand icon, pulsing arrow, and text reveal — 1080×1920.
Preview
Code
import {
AbsoluteFill,
Composition,
interpolate,
useCurrentFrame,
useVideoConfig,
Easing,
} from "remotion";
// ─── CONFIG ────────────────────────────────────────────────────────────────────
const CONFIG = {
// Colors
bgOverlay: "rgba(0,0,0,0.55)",
gradientStart: "rgba(0,0,0,0.85)",
gradientEnd: "rgba(0,0,0,0)",
accentColor: "#ffffff",
secondaryColor: "rgba(255,255,255,0.55)",
pulseColor: "rgba(255,255,255,0.18)",
// Typography
ctaText: "SWIPE UP",
learnMoreText: "Learn More →",
// Sizing
arrowFontSize: 96,
ctaFontSize: 44,
learnMoreFontSize: 28,
handEmojiFontSize: 72,
pulseSize: 160,
// Animation
arrowBounceAmplitude: 22, // px
handSlideDistance: 220, // px upward travel per loop
pulseMin: 0.88,
pulseMax: 1.12,
loopFrames: 60, // frames per sub-loop (2 s)
};
// ─── HELPERS ───────────────────────────────────────────────────────────────────
/**
* Returns a value that oscillates smoothly between 0 and 1 over `period` frames,
* peaking at frame period/2, using a sine curve.
*/
function sinePingPong(frame: number, period: number): number {
const t = (frame % period) / period; // 0..1
return Math.sin(t * Math.PI); // 0 → 1 → 0
}
// ─── GRADIENT OVERLAY ──────────────────────────────────────────────────────────
const GradientOverlay: React.FC = () => (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
height: "55%",
background: `linear-gradient(to top, ${CONFIG.gradientStart} 0%, ${CONFIG.gradientEnd} 100%)`,
pointerEvents: "none",
}}
/>
);
// ─── BACKGROUND PANEL ──────────────────────────────────────────────────────────
const BackgroundPanel: React.FC<{ opacity: number }> = ({ opacity }) => (
<div
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
top: 0,
background:
"radial-gradient(ellipse at 50% 110%, rgba(80,60,180,0.25) 0%, rgba(0,0,0,0) 65%)",
opacity,
}}
/>
);
// ─── PULSING RING ──────────────────────────────────────────────────────────────
const PulsingRing: React.FC<{ frame: number }> = ({ frame }) => {
const scale = interpolate(
sinePingPong(frame, CONFIG.loopFrames),
[0, 1],
[CONFIG.pulseMin, CONFIG.pulseMax],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
const opacity = interpolate(
sinePingPong(frame, CONFIG.loopFrames),
[0, 0.5, 1],
[0.5, 1, 0.5],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<div
style={{
position: "absolute",
width: CONFIG.pulseSize,
height: CONFIG.pulseSize,
borderRadius: "50%",
border: `3px solid ${CONFIG.pulseColor}`,
backgroundColor: "rgba(255,255,255,0.05)",
transform: `scale(${scale})`,
opacity,
}}
/>
);
};
// ─── BOUNCING ARROW ────────────────────────────────────────────────────────────
const BouncingArrow: React.FC<{ frame: number }> = ({ frame }) => {
const bounce = interpolate(
sinePingPong(frame, CONFIG.loopFrames),
[0, 1],
[0, -CONFIG.arrowBounceAmplitude],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" }
);
return (
<div
style={{
fontSize: CONFIG.arrowFontSize,
lineHeight: 1,
color: CONFIG.accentColor,
transform: `translateY(${bounce}px)`,
userSelect: "none",
textShadow: "0 0 40px rgba(255,255,255,0.4)",
}}
>
↑
</div>
);
};
// ─── HAND EMOJI (SLIDING UP LOOP) ──────────────────────────────────────────────
const HandEmoji: React.FC<{ frame: number }> = ({ frame }) => {
const loopProgress = (frame % CONFIG.loopFrames) / CONFIG.loopFrames; // 0..1
// Ease in-out for the slide; reset sharply at loop boundary
const eased = Easing.inOut(Easing.sin)(loopProgress);
const translateY = interpolate(eased, [0, 1], [0, -CONFIG.handSlideDistance], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// Fade out near the end of each loop so the reset is invisible
const opacity = interpolate(loopProgress, [0, 0.1, 0.8, 1], [0, 1, 1, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
fontSize: CONFIG.handEmojiFontSize,
lineHeight: 1,
transform: `translateY(${translateY}px)`,
opacity,
userSelect: "none",
}}
>
☝️
</div>
);
};
// ─── LEARN MORE LABEL ──────────────────────────────────────────────────────────
const LearnMoreLabel: React.FC<{ frame: number }> = ({ frame }) => {
// Fade in during first 20 frames
const opacity = interpolate(frame, [0, 20], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
fontSize: CONFIG.learnMoreFontSize,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 400,
color: CONFIG.secondaryColor,
letterSpacing: 2,
opacity,
marginBottom: 18,
}}
>
{CONFIG.learnMoreText}
</div>
);
};
// ─── SWIPE UP TEXT ─────────────────────────────────────────────────────────────
const SwipeUpText: React.FC<{ frame: number }> = ({ frame }) => {
// Slide up + fade in during first 25 frames
const opacity = interpolate(frame, [5, 25], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const translateY = interpolate(frame, [5, 25], [20, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.out(Easing.quad),
});
return (
<div
style={{
fontSize: CONFIG.ctaFontSize,
fontFamily: "system-ui, -apple-system, sans-serif",
fontWeight: 800,
color: CONFIG.accentColor,
letterSpacing: 10,
textTransform: "uppercase" as const,
opacity,
transform: `translateY(${translateY}px)`,
marginTop: 16,
textShadow: "0 2px 20px rgba(0,0,0,0.6)",
}}
>
{CONFIG.ctaText}
</div>
);
};
// ─── MAIN COMPOSITION ──────────────────────────────────────────────────────────
export const SwipeCTA: React.FC = () => {
const frame = useCurrentFrame();
const { width, height } = useVideoConfig();
// Background panel fades in over first 30 frames
const bgOpacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
// CTA group sits at 80% of vertical height
const ctaTop = height * 0.80;
return (
<AbsoluteFill
style={{
backgroundColor: "rgba(18,12,36,1)",
width,
height,
overflow: "hidden",
}}
>
{/* Ambient background radial glow */}
<BackgroundPanel opacity={bgOpacity} />
{/* Bottom gradient overlay */}
<GradientOverlay />
{/* CTA group — centered horizontally, anchored to 80% height */}
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: ctaTop,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-start",
}}
>
{/* "Learn More →" label */}
<LearnMoreLabel frame={frame} />
{/* Pulse ring + arrow stacked in same slot */}
<div
style={{
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
width: CONFIG.pulseSize,
height: CONFIG.pulseSize,
}}
>
<PulsingRing frame={frame} />
<BouncingArrow frame={frame} />
</div>
{/* "SWIPE UP" text */}
<SwipeUpText frame={frame} />
{/* Hand emoji slides up from below text */}
<div
style={{
marginTop: 28,
height: 80,
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
}}
>
<HandEmoji frame={frame} />
</div>
</div>
</AbsoluteFill>
);
};
// ─── REMOTION ROOT ─────────────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
<Composition
id="SwipeCTA"
component={SwipeCTA}
durationInFrames={180}
fps={30}
width={1080}
height={1920}
/>
);Swipe-Up CTA Animation
A vertical 1080×1920 looping animation designed for Instagram Stories, TikTok, and YouTube Shorts. It renders a polished swipe-up call-to-action overlay: a dark gradient fades up from the bottom, a pulsing ring sits behind a bouncing upward arrow, bold “SWIPE UP” text reveals below it, and a hand cursor emoji slides upward on repeat. A subtle “Learn More →” label sits above the arrow. The entire 6-second loop can be composited as an overlay on any video background.
Composition specs
| Property | Value |
|---|---|
| Resolution | 1080 × 1920 (vertical) |
| FPS | 30 |
| Duration | 180 frames (6 s loop) |
Elements
- Dark-to-transparent gradient overlay anchored to the bottom of the frame
- Pulsing ring that oscillates between 0.9× and 1.1× scale behind the arrow
- Upward-pointing arrow (↑) that bounces vertically via a sine easing loop
- Bold white “SWIPE UP” label with wide letter-spacing beneath the arrow
- Hand cursor emoji (☝️) that slides upward and resets in a seamless loop
- Subtle “Learn More →” secondary label above the arrow
- Semi-transparent blurred gradient background panel