← Catalog
Edit Props (live)
IMAGE
IMAGE
TEXT
TEXT
TEXT
config
config
config
config
config
config
config
Source
212 linesimport React from "react";
import {
AbsoluteFill,
Img,
useCurrentFrame,
useVideoConfig,
interpolate,
Easing,
spring,
staticFile,
} from "remotion";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
loadInter("normal", { weights: ["400", "500", "700", "900"] });
interface LogoSpotlightProps {
logo: string; // file path or URL
wordmark?: string; // optional text label
wordmarkLayout?: "below" | "side" | "none"; // default "below"
tile_bg?: string; // dark chip for monochrome logos
backgroundColor?: string; // scene bg, default "#f2eee4"
accentColor?: string; // glow/accent behind tile, default "#ffffff"
iconScale?: number; // 0-1 scale of viewport width. Defaults: 0.36 (below), 0.26 (side)
positionY?: number; // 0-1 vertical anchor. Default 0.45 (slightly higher than center).
captionText?: string;
captionStyle?: "italic" | "bold";
captionPosition?: "top" | "bottom";
fontColor?: string; // wordmark + caption color, default "#111"
}
const FONT = "'Inter', sans-serif";
export const LogoSpotlight: React.FC<LogoSpotlightProps> = ({
logo,
wordmark,
wordmarkLayout = "below",
tile_bg,
backgroundColor = "#f2eee4",
accentColor = "#ffffff",
iconScale,
positionY = 0.45,
captionText,
captionStyle = "italic",
captionPosition = "top",
fontColor = "#111",
}) => {
const frame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
// Layout-aware default sizes — below gets a bigger icon, side has to share
// horizontal space with the wordmark so the icon must be smaller.
const effectiveScale = iconScale ?? (wordmarkLayout === "side" ? 0.26 : 0.36);
const iconSize = Math.floor(width * effectiveScale);
const resolved = logo.startsWith("http") ? logo : staticFile(logo);
// Spring in with scale + slight overshoot
const enter = spring({
frame: frame - 4,
fps,
config: { mass: 1, damping: 11, stiffness: 120 },
});
const scale = interpolate(enter, [0, 1], [0.3, 1]);
const opacity = interpolate(frame, [0, 14], [0, 1], { extrapolateRight: "clamp" });
// Subtle float + rotation after entry for life (kept gentle so icon never
// bleeds off the sides)
const t = frame / fps;
const floatY = Math.sin(t * 1.3) * 6;
const rotate = Math.sin(t * 0.9) * 0.6;
// Wordmark enters a beat after the icon
const wmEnter = spring({
frame: frame - 16,
fps,
config: { mass: 1, damping: 14, stiffness: 140 },
});
const wmY = interpolate(wmEnter, [0, 1], [40, 0]);
const wmOp = interpolate(wmEnter, [0, 0.4, 1], [0, 1, 1]);
// Caption enters almost immediately
const capOp = interpolate(frame, [2, 16], [0, 1], { extrapolateRight: "clamp" });
const capY = interpolate(frame, [2, 16], [10, 0], {
extrapolateRight: "clamp",
easing: Easing.out(Easing.cubic),
});
// Wordmark font scales with icon. In side layout it has to share horizontal
// space so it can't be too big — set conservatively so a 6-char word fits.
const wordmarkFontSize = wordmarkLayout === "side"
? Math.floor(iconSize * 0.58)
: Math.floor(iconSize * 0.34);
const tileRadius = Math.floor(iconSize * 0.22);
const iconNode = tile_bg ? (
<div
style={{
width: iconSize,
height: iconSize,
borderRadius: tileRadius,
background: tile_bg,
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
boxShadow: "0 30px 60px rgba(0,0,0,0.25), 0 10px 20px rgba(0,0,0,0.15)",
}}
>
<Img src={resolved} style={{ width: "68%", height: "68%", objectFit: "contain" }} />
</div>
) : (
<Img
src={resolved}
style={{
width: iconSize,
height: iconSize,
objectFit: "contain",
filter: "drop-shadow(0 22px 40px rgba(0,0,0,0.22))",
}}
/>
);
return (
<AbsoluteFill style={{ backgroundColor, overflow: "hidden" }}>
{/* Soft accent glow behind the logo */}
<div
style={{
position: "absolute",
top: `${positionY * 100}%`,
left: "50%",
width: iconSize * 1.8,
height: iconSize * 1.8,
transform: "translate(-50%, -50%)",
background: `radial-gradient(circle, ${accentColor}55 0%, transparent 60%)`,
filter: "blur(50px)",
opacity,
pointerEvents: "none",
}}
/>
{/* Caption */}
{captionText && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
top: captionPosition === "top" ? 180 : undefined,
bottom: captionPosition === "bottom" ? 200 : undefined,
textAlign: "center",
fontFamily: FONT,
fontStyle: captionStyle === "italic" ? "italic" : "normal",
fontWeight: captionStyle === "italic" ? 500 : 800,
fontSize: 64,
color: fontColor,
letterSpacing: -1.2,
padding: "0 50px",
opacity: capOp,
transform: `translateY(${capY}px)`,
zIndex: 5,
}}
>
{captionText}
</div>
)}
{/* Logo + wordmark group — anchored at positionY, centered horizontally,
bounded so nothing bleeds off the viewport edges. */}
<div
style={{
position: "absolute",
top: `${positionY * 100}%`,
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "92%",
display: "flex",
flexDirection: wordmarkLayout === "side" ? "row" : "column",
alignItems: "center",
justifyContent: "center",
gap: wordmarkLayout === "side" ? Math.floor(iconSize * 0.18) : Math.floor(iconSize * 0.1),
}}
>
<div
style={{
transform: `translateY(${floatY}px) rotate(${rotate}deg) scale(${scale})`,
opacity,
}}
>
{iconNode}
</div>
{wordmark && wordmarkLayout !== "none" && (
<div
style={{
fontFamily: FONT,
fontSize: wordmarkFontSize,
fontWeight: 800,
color: fontColor,
letterSpacing: -2.5,
lineHeight: 1,
opacity: wmOp,
transform: `translateY(${wmY}px)`,
}}
>
{wordmark}
</div>
)}
</div>
</AbsoluteFill>
);
};
Schema (Zod)
// Auto-extracted from Props interface — see source for authoritative types.
// Component: LogoSpotlight
// 12 props detected · 5 port(s)
Logo Spotlight
logo_spotlightLogo Spotlight — Andy's hand-crafted Remotion scene. 12 props. logo:IMAGE, iconScale:IMAGE, captionText:TEXT, +2 more.
Workflow Inputs (5)
- Logo
logoIMAGERequired - Icon Scale
iconScaleIMAGE - Caption Text
captionTextTEXT - Caption Style
captionStyleTEXT - Caption Position
captionPositionTEXT
Config Fields (7)
tile_bgwordmarkfontColorpositionYaccentColorwordmarkLayoutbackgroundColor
Meta
- Updated
- 4/21/2026