reelcn
GitHubBack to landing

Spotlight

Soft pool of accent light that drifts slowly or holds a position, with a vignette that darkens the edges.

Spotlight, drift
Spotlight, drift thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/spotlight.json

Files land in src/reelcn/ with the items it builds on. Dependencies: remotion.

Usage

<AbsoluteFill>
  <Spotlight />
  <Center><TextReveal text="Hello" /></Center>
</AbsoluteFill>

Props

PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
backgroundstringBase fill. Defaults to the theme background.
colorstringLight color. Defaults to the theme accent.
intensitynumberOpacity at the center of the light, 0–1. Default 0.35.
position{ x: number; y: number; }Pin the light at fractions of the canvas (0–1). Omit to let it drift on a slow figure-eight.
sizenumberLight radius in design units. Default 820.
vignettenumberHow dark the corners get, 0–1. Default 0.4.
speednumberDrift speed multiplier. 0 holds the light still. Default 1.
styleCSSProperties
classNamestring

Built from

Use

  • Product hero shots, single-idea statements and dramatic reveals
  • Drawing the eye to one area of the frame without a hard shape

Avoid

  • Several directional light shafts. Use beams

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/spotlight.tsx
/**
 * @title Spotlight
 * @category backgrounds
 * @description Soft pool of accent light that drifts slowly or holds a position, with a vignette that darkens the edges.
 * @duration sustained
 * @use Product hero shots, single-idea statements and dramatic reveals
 * @use Drawing the eye to one area of the frame without a hard shape
 * @avoid Several directional light shafts. Use `beams`
 * @tags spotlight, light, vignette, stage, focus
 * @example
 * <AbsoluteFill>
 *   <Spotlight />
 *   <Center><TextReveal text="Hello" /></Center>
 * </AbsoluteFill>
 */
import type React from "react";
import { AbsoluteFill } from "remotion";
import { alpha, type MotionProps, useMotion, useTheme, useViewport } from "./core";
import { FlatAccentLook, FlatBlocksLook, GrainFieldLook, GridSweepLook } from "./core-physical-light";

export type SpotlightProps = MotionProps & {
  /** Base fill. Defaults to the theme background. */
  background?: string;
  /** Light color. Defaults to the theme accent. */
  color?: string;
  /** Opacity at the center of the light, 0–1. */
  intensity?: number;
  /** Pin the light at fractions of the canvas (0–1). Omit to let it drift on a slow figure-eight. */
  position?: { x: number; y: number };
  /** Light radius in design units. */
  size?: number;
  /** How dark the corners get, 0–1. */
  vignette?: number;
  /** Drift speed multiplier. 0 holds the light still. */
  speed?: number;
  style?: React.CSSProperties;
  className?: string;
};

type SpotlightLook = "pool" | "grid-sweep" | "grain-field" | "flat-blocks" | "flat-accent";

const LOOK_BY_THEME: Record<string, SpotlightLook> = {
  daylight: "pool",
  midnight: "grid-sweep",
  mono: "flat-accent",
  ledger: "grain-field",
  afterglow: "pool",
  chromewave: "flat-blocks",
};

const clamp01 = (value: number) => Math.min(Math.max(value, 0), 1);

export function Spotlight({
  background,
  color,
  intensity = 0.35,
  position,
  size = 820,
  vignette = 0.4,
  speed = 1,
  style,
  className,
  ...motion
}: SpotlightProps) {
  const theme = useTheme();
  const { width, height, u } = useViewport();
  const m = useMotion(motion);
  const light = color ?? (theme.name === "daylight" ? theme.colors.foreground : theme.colors.accent);
  const seconds = (m.frame / m.fps) * speed;
  // A 1:2 Lissajous curve: a lazy figure-eight around the upper middle of the frame.
  const x = position ? position.x : 0.5 + 0.2 * Math.sin(seconds * 0.5);
  const y = position ? position.y : 0.42 + 0.09 * Math.sin(seconds + 0.6);
  const radius = u(size) * (1 + 0.04 * Math.sin((m.frame / m.fps) * 0.9));
  const strength = intensity * m.presence;

  const look = LOOK_BY_THEME[theme.name] ?? "pool";

  if (look === "grid-sweep")
    return (
      <GridSweepLook
        theme={theme}
        seconds={seconds}
        strength={strength}
        background={background}
        className={className}
        style={style}
      />
    );
  if (look === "grain-field")
    return (
      <GrainFieldLook
        theme={theme}
        seconds={seconds}
        strength={strength}
        palette={[light]}
        background={background}
        className={className}
        style={style}
      />
    );
  if (look === "flat-blocks")
    return (
      <FlatBlocksLook
        theme={theme}
        seconds={seconds}
        strength={strength}
        palette={[light, theme.colors.highlight]}
        background={background}
        className={className}
        style={style}
      />
    );
  if (look === "flat-accent")
    return (
      <FlatAccentLook
        theme={theme}
        seconds={seconds}
        strength={strength}
        background={background}
        className={className}
        style={style}
      />
    );

  const stop = (share: number) => alpha(light, clamp01(strength * share));
  const pool = `radial-gradient(circle ${radius}px at ${x * width}px ${y * height}px, ${stop(1)} 0%, ${stop(0.62)} 22%, ${stop(0.28)} 48%, ${stop(0.08)} 75%, ${stop(0)} 100%)`;
  const edges = `radial-gradient(ellipse farthest-corner at 50% 50%, ${alpha("#000000", 0)} 45%, ${alpha("#000000", clamp01(vignette))} 100%)`;

  return (
    <AbsoluteFill
      className={className}
      style={{
        backgroundColor: background ?? theme.colors.background,
        backgroundImage: `${edges}, ${pool}`,
        ...style,
      }}
    />
  );
}

On this page