StealThis .dev

Webinar Promo (Remotion)

A 4-second webinar promo animation on a rich teal-and-slate palette — a pulsing FREE WEBINAR badge anchors the top-left while the session title sweeps in with a spring entrance, the host avatar and bio fade up beneath it, a date-time chip slides in from the right, a Save Your Spot button springs into view, and a live viewer counter animates from 0 to 1,247 to drive urgency.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

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

// ── Config ────────────────────────────────────────────────────────────────────
const WEBINAR_TITLE = "Mastering Modern React in 2026";
const HOST_NAME = "Sarah Chen";
const HOST_ROLE = "Senior Engineer · Vercel";
const HOST_INITIALS = "SC";
const DATE_TIME = "Tue Jun 18  /  3:00 PM EST";
const BUTTON_LABEL = "Save Your Spot";
const VIEWER_COUNT = 1247;
const BRAND_COLOR = "#0d9488";        // teal-600
const BRAND_GLOW = "#0d948840";       // teal with alpha
const BRAND_LIGHT = "#2dd4bf";        // teal-400 highlight
const BG_COLOR = "#0f172a";           // slate-900
const CARD_COLOR = "#1e293b";         // slate-800
const BADGE_BG = "#134e4a";          // dark teal badge bg
const BADGE_BORDER = "#0d9488";

// ── Background glow layer ─────────────────────────────────────────────────────
const BackgroundGlow: React.FC<{ frame: number }> = ({ frame }) => {
  const pulse = Math.sin((frame / 30) * Math.PI * 0.6) * 0.12 + 0.88;
  return (
    <>
      {/* Main center glow */}
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          width: 900,
          height: 600,
          borderRadius: "50%",
          transform: `translate(-50%, -50%) scale(${pulse})`,
          background: `radial-gradient(ellipse at center, ${BRAND_COLOR}18 0%, ${BRAND_COLOR}08 45%, transparent 70%)`,
          pointerEvents: "none",
        }}
      />
      {/* Top-left accent glow behind badge */}
      <div
        style={{
          position: "absolute",
          top: -60,
          left: -60,
          width: 340,
          height: 340,
          borderRadius: "50%",
          background: `radial-gradient(circle, ${BRAND_COLOR}22 0%, transparent 65%)`,
          pointerEvents: "none",
        }}
      />
      {/* Bottom-right subtle glow */}
      <div
        style={{
          position: "absolute",
          bottom: -80,
          right: -80,
          width: 280,
          height: 280,
          borderRadius: "50%",
          background: `radial-gradient(circle, #6366f112 0%, transparent 65%)`,
          pointerEvents: "none",
        }}
      />
    </>
  );
};

// ── FREE WEBINAR badge ────────────────────────────────────────────────────────
const WebinarBadge: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const scale = spring({
    frame,
    fps,
    from: 0,
    to: 1,
    config: { damping: 10, stiffness: 200, mass: 0.5 },
  });
  const opacity = interpolate(frame, [0, 8], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Pulse glow ring
  const pulseScale = 1 + Math.sin((frame / 20) * Math.PI) * 0.06;
  const pulseOpacity = 0.4 + Math.sin((frame / 20) * Math.PI) * 0.3;

  return (
    <div
      style={{
        position: "absolute",
        top: 52,
        left: 64,
        opacity,
        transform: `scale(${scale})`,
        transformOrigin: "left center",
      }}
    >
      {/* Pulsing glow ring behind badge */}
      <div
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          width: 180,
          height: 50,
          borderRadius: 50,
          transform: `translate(-50%, -50%) scale(${pulseScale})`,
          background: BRAND_GLOW,
          opacity: pulseOpacity,
          filter: "blur(8px)",
          pointerEvents: "none",
        }}
      />
      <div
        style={{
          position: "relative",
          display: "flex",
          alignItems: "center",
          gap: 8,
          backgroundColor: BADGE_BG,
          border: `1.5px solid ${BADGE_BORDER}`,
          borderRadius: 50,
          padding: "10px 20px",
        }}
      >
        {/* Live dot */}
        <div
          style={{
            width: 8,
            height: 8,
            borderRadius: "50%",
            backgroundColor: BRAND_LIGHT,
            boxShadow: `0 0 6px ${BRAND_LIGHT}`,
            flexShrink: 0,
          }}
        />
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 800,
            fontSize: 13,
            color: BRAND_LIGHT,
            letterSpacing: 2.5,
            textTransform: "uppercase" as const,
          }}
        >
          Free Webinar
        </span>
      </div>
    </div>
  );
};

// ── Webinar title ─────────────────────────────────────────────────────────────
const WebinarTitle: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 10);
  const translateX = spring({
    frame: delayed,
    fps,
    from: -80,
    to: 0,
    config: { damping: 18, stiffness: 130, mass: 0.9 },
  });
  const opacity = interpolate(delayed, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Split title into two lines for layout
  const line1 = "Mastering Modern React";
  const line2 = "in 2026";

  return (
    <div
      style={{
        position: "absolute",
        top: 160,
        left: 64,
        right: 64,
        opacity,
        transform: `translateX(${translateX}px)`,
      }}
    >
      <div
        style={{
          fontFamily: "system-ui, -apple-system, sans-serif",
          fontWeight: 900,
          fontSize: 68,
          color: "#ffffff",
          letterSpacing: -2.5,
          lineHeight: 1.05,
          textShadow: "0 4px 32px rgba(0,0,0,0.6)",
        }}
      >
        <div>{line1}</div>
        <div style={{ color: BRAND_LIGHT }}>{line2}</div>
      </div>
    </div>
  );
};

// ── Host info block ───────────────────────────────────────────────────────────
const HostInfo: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 25);
  const translateY = spring({
    frame: delayed,
    fps,
    from: 24,
    to: 0,
    config: { damping: 14, stiffness: 100 },
  });
  const opacity = interpolate(delayed, [0, 20], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 430,
        left: 64,
        display: "flex",
        alignItems: "center",
        gap: 18,
        opacity,
        transform: `translateY(${translateY}px)`,
      }}
    >
      {/* Avatar circle */}
      <div
        style={{
          width: 60,
          height: 60,
          borderRadius: "50%",
          background: `linear-gradient(135deg, ${BRAND_COLOR} 0%, #0f766e 100%)`,
          border: `2px solid ${BRAND_LIGHT}`,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          flexShrink: 0,
          boxShadow: `0 0 16px ${BRAND_COLOR}60`,
        }}
      >
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 700,
            fontSize: 18,
            color: "#ffffff",
            letterSpacing: 0.5,
          }}
        >
          {HOST_INITIALS}
        </span>
      </div>

      {/* Name + role */}
      <div>
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 700,
            fontSize: 20,
            color: "#ffffff",
            letterSpacing: -0.3,
          }}
        >
          {HOST_NAME}
        </div>
        <div
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 400,
            fontSize: 15,
            color: "rgba(255,255,255,0.48)",
            marginTop: 2,
            letterSpacing: 0.2,
          }}
        >
          {HOST_ROLE}
        </div>
      </div>
    </div>
  );
};

// ── Date/time chip ────────────────────────────────────────────────────────────
const DateTimeChip: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 40);
  const translateX = spring({
    frame: delayed,
    fps,
    from: 120,
    to: 0,
    config: { damping: 15, stiffness: 140 },
  });
  const opacity = interpolate(delayed, [0, 15], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 52,
        right: 64,
        opacity,
        transform: `translateX(${translateX}px)`,
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: 10,
          backgroundColor: CARD_COLOR,
          border: "1px solid rgba(255,255,255,0.08)",
          borderRadius: 12,
          padding: "12px 22px",
          boxShadow: "0 4px 24px rgba(0,0,0,0.3)",
        }}
      >
        {/* Calendar icon placeholder */}
        <div
          style={{
            width: 18,
            height: 18,
            borderRadius: 4,
            border: `1.5px solid ${BRAND_COLOR}`,
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            flexShrink: 0,
          }}
        >
          <div
            style={{
              width: 8,
              height: 1,
              backgroundColor: BRAND_COLOR,
              marginTop: 3,
            }}
          />
        </div>
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 600,
            fontSize: 15,
            color: "rgba(255,255,255,0.85)",
            letterSpacing: 0.4,
            whiteSpace: "nowrap" as const,
          }}
        >
          {DATE_TIME}
        </span>
      </div>
    </div>
  );
};

// ── Save Your Spot button ─────────────────────────────────────────────────────
const SaveSpotButton: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 45);
  const scale = spring({
    frame: delayed,
    fps,
    from: 0,
    to: 1,
    config: { damping: 10, stiffness: 200, mass: 0.55 },
  });
  const opacity = interpolate(delayed, [0, 10], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Shimmer sweep — loops every 60 frames after button appears
  const shimmerProgress = delayed > 20 ? ((frame - 65) % 70) / 70 : -0.5;
  const shimmerX = interpolate(shimmerProgress, [0, 1], [-60, 320], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        bottom: 100,
        left: 64,
        opacity,
        transform: `scale(${scale})`,
        transformOrigin: "left bottom",
      }}
    >
      <div
        style={{
          position: "relative",
          overflow: "hidden",
          borderRadius: 14,
          background: `linear-gradient(135deg, ${BRAND_COLOR} 0%, #0f766e 100%)`,
          padding: "18px 36px",
          display: "inline-flex",
          alignItems: "center",
          gap: 12,
          boxShadow: `0 8px 32px ${BRAND_COLOR}50, 0 2px 8px rgba(0,0,0,0.4)`,
        }}
      >
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 700,
            fontSize: 20,
            color: "#ffffff",
            letterSpacing: 0.2,
          }}
        >
          {BUTTON_LABEL}
        </span>
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontSize: 20,
            color: "rgba(255,255,255,0.9)",
          }}
        >

        </span>

        {/* Shimmer sweep */}
        {delayed > 20 && (
          <div
            style={{
              position: "absolute",
              top: 0,
              left: shimmerX,
              width: 60,
              height: "100%",
              background:
                "linear-gradient(90deg, transparent, rgba(255,255,255,0.22), transparent)",
              transform: "skewX(-18deg)",
              pointerEvents: "none",
            }}
          />
        )}
      </div>
    </div>
  );
};

// ── Live viewer counter ───────────────────────────────────────────────────────
const ViewerCounter: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 50);
  const opacity = interpolate(delayed, [0, 18], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const translateY = spring({
    frame: delayed,
    fps,
    from: 16,
    to: 0,
    config: { damping: 14, stiffness: 120 },
  });

  // Animate count from 0 to VIEWER_COUNT over 60 frames
  const countProgress = interpolate(delayed, [0, 60], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });
  const currentCount = Math.floor(countProgress * VIEWER_COUNT);
  const formatted = currentCount.toLocaleString("en-US");

  return (
    <div
      style={{
        position: "absolute",
        bottom: 108,
        right: 64,
        opacity,
        transform: `translateY(${translateY}px)`,
        display: "flex",
        alignItems: "center",
        gap: 10,
      }}
    >
      {/* Pulsing dot */}
      <div
        style={{
          width: 9,
          height: 9,
          borderRadius: "50%",
          backgroundColor: "#f87171",
          boxShadow: "0 0 8px #f87171",
          flexShrink: 0,
        }}
      />
      <div>
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 800,
            fontSize: 22,
            color: "#ffffff",
            letterSpacing: -0.5,
          }}
        >
          {formatted}
        </span>
        <span
          style={{
            fontFamily: "system-ui, -apple-system, sans-serif",
            fontWeight: 400,
            fontSize: 14,
            color: "rgba(255,255,255,0.45)",
            marginLeft: 8,
            letterSpacing: 0.3,
          }}
        >
          watching
        </span>
      </div>
    </div>
  );
};

// ── Divider line ──────────────────────────────────────────────────────────────
const DividerLine: React.FC<{ frame: number; fps: number }> = ({ frame, fps }) => {
  const delayed = Math.max(0, frame - 30);
  const scaleX = spring({
    frame: delayed,
    fps,
    from: 0,
    to: 1,
    config: { damping: 20, stiffness: 100 },
  });
  const opacity = interpolate(delayed, [0, 12], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  return (
    <div
      style={{
        position: "absolute",
        top: 410,
        left: 64,
        width: 600,
        height: 1,
        opacity,
        transform: `scaleX(${scaleX})`,
        transformOrigin: "left center",
        background: `linear-gradient(90deg, ${BRAND_COLOR}80 0%, transparent 100%)`,
      }}
    />
  );
};

// ── Main composition ──────────────────────────────────────────────────────────
export const WebinarPromo: 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,
        overflow: "hidden",
        opacity: globalOpacity,
      }}
    >
      {/* Layer 0 — background glows */}
      <BackgroundGlow frame={frame} />

      {/* Layer 1 — badge */}
      <WebinarBadge frame={frame} fps={fps} />

      {/* Layer 2 — date/time chip */}
      <DateTimeChip frame={frame} fps={fps} />

      {/* Layer 3 — main title */}
      <WebinarTitle frame={frame} fps={fps} />

      {/* Layer 4 — divider */}
      <DividerLine frame={frame} fps={fps} />

      {/* Layer 5 — host info */}
      <HostInfo frame={frame} fps={fps} />

      {/* Layer 6 — CTA button */}
      <SaveSpotButton frame={frame} fps={fps} />

      {/* Layer 7 — viewer counter */}
      <ViewerCounter frame={frame} fps={fps} />
    </AbsoluteFill>
  );
};

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

Webinar Promo

A cinematic 4-second promo clip designed to promote a live webinar. The composition opens on a deep slate background (#1e293b) with a layered teal glow radiating from center. A pulsing FREE WEBINAR badge springs into the top-left corner at frame 0, drawing the eye before the session title — “Mastering Modern React in 2026” — slides in from the left with a damped spring and a soft drop-shadow.

Starting at frame 25, the host block fades in below the title: a teal-bordered avatar circle sits beside the presenter’s name and role in muted white typography. At frame 40 a date/time chip (“Tue Jun 18 / 3:00 PM EST”) slides in from the right edge, completing the contextual information. A “Save Your Spot →” button then springs upward from scale 0, styled in solid teal with a shimmer sweep on loop. The final touch is an animated live viewer counter that ticks from 0 to 1,247 over 60 frames, reinforcing social proof before the composition fades out in the last 20 frames.

All configuration constants sit at the top of the file — swap WEBINAR_TITLE, HOST_NAME, HOST_ROLE, DATE_TIME, BRAND_COLOR, and VIEWER_COUNT to repurpose the template for any event in seconds.

Composition specs

PropertyValue
Resolution1280 × 720
FPS30
Duration4 s (120 frames)

Timeline

TimeAction
0–0.3 s (0–10 f)FREE WEBINAR badge springs in top-left; background glow pulses
0.3–1.0 s (10–30 f)Webinar title slides in from left with spring damping
0.8–1.5 s (25–45 f)Host avatar + name + role fade up beneath the title
1.3–1.8 s (40–55 f)Date/time chip slides in from the right edge
1.5–2.2 s (45–65 f)Save Your Spot button springs up; viewer counter begins ticking
2.2–4.0 s (65–120 f)All elements hold; shimmer sweeps button; counter reaches 1,247; fade-out at frame 100