StealThis .dev
Remotion Medium

YouTube Outro Animation (Remotion)

A 20-second YouTube outro with "thanks for watching", an animated subscribe button pulse, and placeholder end-card slots — rendered with Remotion.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import React from "react";
import {
  AbsoluteFill,
  Composition,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
  Easing,
  Sequence,
} from "remotion";

// ── Config ────────────────────────────────────────────────────────────
const CHANNEL_NAME = "My Channel";
const BRAND_COLOR = "#6366f1";
const BG_COLOR = "#0a0a0f";

// ── "Thanks for watching" ─────────────────────────────────────────────
const ThankYouText: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const opacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const translateY = spring({
    frame,
    fps,
    from: 30,
    to: 0,
    config: { damping: 14, stiffness: 80 },
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 120,
        left: 0,
        right: 0,
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        opacity,
        transform: `translateY(${translateY}px)`,
      }}
    >
      <div
        style={{
          fontFamily: "system-ui, -apple-system, sans-serif",
          fontWeight: 800,
          fontSize: 56,
          color: "#ffffff",
          letterSpacing: -1,
        }}
      >
        Thanks for watching!
      </div>
      <div
        style={{
          fontFamily: "system-ui, -apple-system, sans-serif",
          fontWeight: 400,
          fontSize: 22,
          color: "rgba(255,255,255,0.5)",
          marginTop: 10,
          letterSpacing: 1,
        }}
      >
        {CHANNEL_NAME}
      </div>
    </div>
  );
};

// ── Subscribe button ──────────────────────────────────────────────────
const SubscribeButton: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const scale = spring({
    frame,
    fps,
    from: 0,
    to: 1,
    config: { damping: 12, stiffness: 150, mass: 0.8 },
  });

  // Pulse every 2 seconds after frame 120
  const pulseT = ((frame - 90) % 60) / 60;
  const pulse = frame > 90 ? 1 + 0.06 * Math.sin(pulseT * Math.PI * 2) : 1;

  return (
    <div
      style={{
        position: "absolute",
        top: 280,
        left: "50%",
        transform: `translateX(-50%) scale(${scale * pulse})`,
        transformOrigin: "center center",
      }}
    >
      <div
        style={{
          backgroundColor: "#ff0000",
          borderRadius: 4,
          padding: "18px 48px",
          display: "flex",
          alignItems: "center",
          gap: 12,
        }}
      >
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 700,
            fontSize: 22,
            color: "#ffffff",
            letterSpacing: 0.5,
          }}
        >
          SUBSCRIBE
        </div>
      </div>
    </div>
  );
};

// ── End card placeholders ─────────────────────────────────────────────
const EndCard: React.FC<{
  frame: number;
  fps: number;
  label: string;
  offsetX: number;
}> = ({ frame, fps, label, offsetX }) => {
  const translateX = spring({
    frame,
    fps,
    from: offsetX > 0 ? 60 : -60,
    to: 0,
    config: { damping: 16, stiffness: 100 },
  });
  const opacity = interpolate(frame, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        bottom: 100,
        left: `calc(50% + ${offsetX}px)`,
        transform: `translateX(-50%) translateX(${translateX}px)`,
        opacity,
      }}
    >
      <div
        style={{
          width: 260,
          height: 150,
          borderRadius: 8,
          backgroundColor: "rgba(255,255,255,0.08)",
          border: `2px solid ${BRAND_COLOR}`,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 600,
            fontSize: 16,
            color: "rgba(255,255,255,0.5)",
          }}
        >
          {label}
        </div>
      </div>
    </div>
  );
};

// ── Main composition ──────────────────────────────────────────────────
export const YoutubeOutro: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();

  const bgOpacity = interpolate(frame, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <AbsoluteFill style={{ backgroundColor: BG_COLOR, opacity: bgOpacity }}>
      {/* Background glow */}
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          width: 600,
          height: 600,
          borderRadius: "50%",
          transform: "translate(-50%, -50%)",
          background: `radial-gradient(circle, ${BRAND_COLOR}22 0%, transparent 70%)`,
        }}
      />

      <ThankYouText frame={frame} fps={fps} />

      {/* Subscribe button appears at frame 60 */}
      <Sequence from={60}>
        <SubscribeButton frame={Math.max(0, frame - 60)} fps={fps} />
      </Sequence>

      {/* End cards appear at frame 120 */}
      <Sequence from={120}>
        <EndCard frame={Math.max(0, frame - 120)} fps={fps} label="Watch Next" offsetX={-160} />
        <EndCard frame={Math.max(0, frame - 120)} fps={fps} label="More Videos" offsetX={160} />
      </Sequence>
    </AbsoluteFill>
  );
};

// ── Remotion Root ─────────────────────────────────────────────────────
export const RemotionRoot: React.FC = () => (
  <Composition
    id="YoutubeOutro"
    component={YoutubeOutro}
    durationInFrames={600}
    fps={30}
    width={1280}
    height={720}
  />
);

YouTube Outro Animation

A 20-second end screen for YouTube videos. Shows a “Thanks for watching” headline, an animated subscribe button that pulses, and two placeholder cards for recommended video links.

Composition specs

PropertyValue
Resolution1280 × 720
FPS30
Duration20 s (600 frames)

Sections

  • 0–2 s — “Thanks for watching” fades in
  • 2–5 s — Subscribe button scales in with spring
  • 5–20 s — End card placeholders slide in; subscribe button pulses

Customization

Set CHANNEL_NAME and BRAND_COLOR at the top of the file.