Catalog

Edit Props (live)

TEXT
IMAGE
TEXT
config
config
config
config

Source

211 lines
import React from "react";
import {
  AbsoluteFill,
  useCurrentFrame,
  useVideoConfig,
  interpolate,
  Easing,
  Img,
  staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";

loadInter("normal", { weights: ["400", "700", "900"] });
loadInter("italic", { weights: ["400", "900"] });

interface QuoteCardProps {
  quote: string;
  author?: string;
  role?: string;
  avatarSrc?: string;
  backgroundColor?: string;
  accentColor?: string;
  textColor?: string;
}

const FONT = "'Inter', sans-serif";

export const QuoteCard: React.FC<QuoteCardProps> = ({
  quote,
  author,
  role,
  avatarSrc,
  backgroundColor = "#f6f4ed",
  accentColor = "#c94f3d",
  textColor = "#1a1a1a",
}) => {
  const frame = useCurrentFrame();
  const { durationInFrames } = useVideoConfig();

  // Split quote into words for stagger reveal
  const words = quote.split(/\s+/);

  // Big quote mark fades in first
  const markOpacity = interpolate(frame, [0, 14], [0, 1], { extrapolateRight: "clamp" });
  const markScale = interpolate(frame, [0, 18], [0.7, 1], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.back(1.5)),
  });

  // Author block reveals at the end
  const authorAt = Math.max(60, Math.round(durationInFrames * 0.55));
  const authorOp = interpolate(frame, [authorAt, authorAt + 14], [0, 1], {
    extrapolateRight: "clamp",
  });
  const authorY = interpolate(frame, [authorAt, authorAt + 18], [20, 0], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Accent rule to left of author
  const ruleW = interpolate(frame, [authorAt + 4, authorAt + 24], [0, 80], {
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  return (
    <AbsoluteFill
      style={{
        backgroundColor,
        color: textColor,
        fontFamily: FONT,
        display: "flex",
        flexDirection: "column",
        justifyContent: "center",
        padding: "100px",
      }}
    >
      {/* Big decorative curly quote mark — Georgia gives a proper typographic glyph */}
      <div
        style={{
          position: "absolute",
          top: -60,
          left: 60,
          fontSize: 480,
          fontWeight: 700,
          color: accentColor,
          lineHeight: 1,
          opacity: markOpacity * 0.18,
          transform: `scale(${markScale})`,
          transformOrigin: "top left",
          fontFamily: "Georgia, 'Times New Roman', serif",
          userSelect: "none",
        }}
      >
        {"\u201C"}
      </div>

      {/* Quote body — word-by-word reveal, vertically centered in the card */}
      <div
        style={{
          position: "relative",
          zIndex: 2,
          fontSize: 72,
          fontWeight: 700,
          fontStyle: "italic",
          lineHeight: 1.2,
          letterSpacing: -1.5,
          color: textColor,
          marginBottom: 80,
        }}
      >
        {words.map((w, i) => {
          const startF = 8 + i * 3;
          const endF = startF + 14;
          const wOp = interpolate(frame, [startF, endF], [0, 1], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
          });
          const wY = interpolate(frame, [startF, endF], [14, 0], {
            extrapolateLeft: "clamp",
            extrapolateRight: "clamp",
            easing: Easing.out(Easing.cubic),
          });
          return (
            <span
              key={i}
              style={{
                display: "inline-block",
                opacity: wOp,
                transform: `translateY(${wY}px)`,
                marginRight: 18,
              }}
            >
              {w}
            </span>
          );
        })}
      </div>

      {/* Author block — sits below the quote, aligned in the centered flex column */}
      {author && (
        <div
          style={{
            position: "relative",
            zIndex: 2,
            display: "flex",
            alignItems: "center",
            gap: 28,
            opacity: authorOp,
            transform: `translateY(${authorY}px)`,
          }}
        >
          {/* Avatar */}
          {avatarSrc && (
            <div
              style={{
                width: 120,
                height: 120,
                borderRadius: "50%",
                overflow: "hidden",
                flexShrink: 0,
                border: `4px solid ${accentColor}`,
              }}
            >
              <Img
                src={avatarSrc.startsWith("http") ? avatarSrc : staticFile(avatarSrc)}
                style={{ width: "100%", height: "100%", objectFit: "cover" }}
              />
            </div>
          )}
          {/* Accent rule (shown only when no avatar) */}
          {!avatarSrc && (
            <div
              style={{
                width: ruleW,
                height: 6,
                background: accentColor,
                flexShrink: 0,
              }}
            />
          )}
          <div style={{ display: "flex", flexDirection: "column" }}>
            <div
              style={{
                fontSize: 44,
                fontWeight: 900,
                letterSpacing: -0.5,
                lineHeight: 1.1,
              }}
            >
              {author}
            </div>
            {role && (
              <div
                style={{
                  fontSize: 28,
                  fontWeight: 400,
                  opacity: 0.65,
                  marginTop: 6,
                }}
              >
                {role}
              </div>
            )}
          </div>
        </div>
      )}
    </AbsoluteFill>
  );
};

Schema (Zod)

// Auto-extracted from Props interface — see source for authoritative types.
// Component: QuoteCard
// 7 props detected · 3 port(s)

Quote Card

quote_card

Quote Card — Andy's hand-crafted Remotion scene. 7 props. quote:TEXT, avatarSrc:IMAGE, textColor:TEXT.

Workflow Inputs (3)

  • Quote
    quote
    TEXTRequired
  • Avatar Src
    avatarSrc
    IMAGE
  • Text Color
    textColor
    TEXT

Config Fields (4)

  • role
  • author
  • accentColor
  • backgroundColor

Meta

Updated
4/21/2026