Image Scene

A single centered image with an optional caption beneath. Good for QR codes, logos, screenshots that need to be focal.

Preview

A single centered image with an optional caption beneath. Good for QR codes, logos, screenshots that need to be focal.

Open editor

Props

NameTypeDefault
srcstring (url)""
captionstring""

Composition

ID
ImageScene
Resolution
1920×1080
FPS
60
Duration
3.0s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import {
  AbsoluteFill,
  Img,
  spring,
  staticFile,
  useVideoConfig,
} from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { proxyExternalImg } from "../../proxy-image";
import { snap } from "../../snap";
import { useDesignFrame } from "../../use-design-frame";

export type ImageSceneProps = {
  src: string;
  caption: string;
  clipStyle?: ClipStyle;
};

/**
 * Resolve an image src to a renderable URL:
 *   - empty → undefined (lets the component render a placeholder)
 *   - data:/blob: → pass through
 *   - http(s) → route through `/api/img/<encoded>` so the export canvas
 *     stays untainted
 *   - bare paths → `staticFile()` so the Remotion bundle server serves
 *     them in both studio + CLI render
 */
function resolveAsset(src: string | undefined): string | undefined {
  if (!src) return undefined;
  if (/^(data:|blob:)/i.test(src)) return src;
  if (/^https?:/i.test(src)) return proxyExternalImg(src);
  return staticFile(src.replace(/^\//, ""));
}

export const ImageScene: React.FC<ImageSceneProps> = ({
  src,
  caption,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { fps } = useVideoConfig();
  const s = resolveClipStyle(clipStyle, {
    background: "#ffffff",
    color: "#0f1014",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: "#0a84ff",
  });

  const enter = spring({
    frame,
    fps,
    config: { damping: 14, stiffness: 110, mass: 0.85 },
  });

  const resolved = resolveAsset(src);
  const trimmedCaption = caption.trim();

  return (
    <AbsoluteFill
      style={{
        background: s.background,
        color: s.color,
        fontFamily: s.fontFamily,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        padding: 80,
      }}
    >
      <div
        style={{
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 28,
          opacity: enter,
          transform: `translate3d(0, ${snap((1 - enter) * 24)}px, 0) scale(${0.92 + enter * 0.08})`,
        }}
      >
        {resolved ? (
          <Img
            src={resolved}
            crossOrigin="anonymous"
            style={{
              maxWidth: 760,
              maxHeight: 760,
              width: "auto",
              height: "auto",
              objectFit: "contain",
              display: "block",
            }}
          />
        ) : null}
        {trimmedCaption ? (
          <div
            style={{
              fontSize: 30,
              fontWeight: 500,
              letterSpacing: "-0.01em",
              textAlign: "center",
              color: s.color,
              maxWidth: 1000,
            }}
          >
            {trimmedCaption}
          </div>
        ) : null}
      </div>
    </AbsoluteFill>
  );
};
Save as ImageScene/ImageScene.tsx