reelcn
GitHubBack to landing

Grid

Line grid that pans slowly, or a perspective floor that scrolls toward the viewer and fades into a glowing horizon.

Grid, flat
Grid, flat thumbnail, 16:9

Installation

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

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

Usage

<AbsoluteFill>
  <Grid perspective />
  <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.
colorstringLine color. Defaults to the theme border (flat) or a translucent accent (perspective).
perspectivebooleanDraw a 3D floor receding to a horizon instead of a flat panning grid. Default false.
cellnumberCell size in design units. Defaults to 80 flat, 110 in perspective.
lineWidthnumberLine width in design units. Default 2.
horizonnumberHorizon height as a share of the canvas height (perspective only). Default 0.5.
glowColorstringGlow along the horizon (perspective only). Defaults to the theme accent; pass "transparent" to remove it.
fadenumberHow strongly the grid fades toward the edges (flat) or into the horizon (perspective), 0–1. Default 0.6.
opacitynumberLine opacity multiplier, 0–1. Default 1.
speednumberPan or scroll speed multiplier. 0 holds the grid still. Default 1.
styleCSSProperties
classNamestring

Built from

Use

  • Tech, developer and product scenes that want structure behind the content
  • Retro or futuristic openers with the perspective floor

Avoid

  • A soft dotted texture. Use dots

Guides

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

Source

registry/items/grid.tsx
/**
 * @title Grid
 * @category backgrounds
 * @description Line grid that pans slowly, or a perspective floor that scrolls toward the viewer and fades into a glowing horizon.
 * @duration sustained
 * @use Tech, developer and product scenes that want structure behind the content
 * @use Retro or futuristic openers with the perspective floor
 * @avoid A soft dotted texture. Use `dots`
 * @tags grid, lines, blueprint, perspective, synthwave, floor
 * @example
 * <AbsoluteFill>
 *   <Grid perspective />
 *   <Center><TextReveal text="Hello" /></Center>
 * </AbsoluteFill>
 */
import type React from "react";
import { AbsoluteFill } from "remotion";
import { alpha, type MotionProps, useMotion, useTheme, useViewport } from "./core";

export type GridProps = MotionProps & {
  /** Base fill. Defaults to the theme background. */
  background?: string;
  /** Line color. Defaults to the theme border (flat) or a translucent accent (perspective). */
  color?: string;
  /** Draw a 3D floor receding to a horizon instead of a flat panning grid. */
  perspective?: boolean;
  /** Cell size in design units. Defaults to 80 flat, 110 in perspective. */
  cell?: number;
  /** Line width in design units. */
  lineWidth?: number;
  /** Horizon height as a share of the canvas height (perspective only). */
  horizon?: number;
  /** Glow along the horizon (perspective only). Defaults to the theme accent; pass "transparent" to remove it. */
  glowColor?: string;
  /** How strongly the grid fades toward the edges (flat) or into the horizon (perspective), 0–1. */
  fade?: number;
  /** Line opacity multiplier, 0–1. */
  opacity?: number;
  /** Pan or scroll speed multiplier. 0 holds the grid still. */
  speed?: number;
  style?: React.CSSProperties;
  className?: string;
};

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

/** Share of the floor depth below which lines are dropped; the fog is fully opaque there, so nothing pops. */
const NEAREST_REL = 0.1;

function flatPath(width: number, height: number, cellPx: number, seconds: number) {
  // Lines stay centered on the canvas and drift diagonally by a fraction of a cell each second.
  const shift = (seconds * 0.3 * cellPx) % cellPx;
  const startX = ((width / 2 + shift) % cellPx) - cellPx;
  const startY = ((height / 2 + shift * 0.6) % cellPx) - cellPx;
  let d = "";
  for (let x = startX; x <= width + cellPx; x += cellPx) d += `M${n(x)} 0V${n(height)}`;
  for (let y = startY; y <= height + cellPx; y += cellPx) d += `M0 ${n(y)}H${n(width)}`;
  return d;
}

function floorPath(width: number, height: number, cellPx: number, horizonY: number, seconds: number) {
  // Pinhole projection of a flat floor: the bottom edge of the canvas is depth 0, the horizon is infinitely far.
  const eye = height - horizonY;
  const focal = eye;
  const maxDepth = focal * (1 / NEAREST_REL - 1);
  const phase = (seconds * 0.6) % 1;
  const cx = width / 2;
  let d = "";
  for (let k = 0; ; k++) {
    const z = (k - phase) * cellPx;
    if (z > maxDepth) break;
    if (z < 0) continue;
    const y = horizonY + (eye * focal) / (focal + z);
    d += `M0 ${n(y)}H${n(width)}`;
  }
  const columns = Math.ceil(width / 2 / NEAREST_REL / cellPx) + 1;
  for (let j = -columns; j <= columns; j++) {
    const x = j * cellPx;
    d += `M${n(cx + x)} ${n(height)}L${n(cx + x * NEAREST_REL)} ${n(horizonY + eye * NEAREST_REL)}`;
  }
  return d;
}

export function Grid({
  background,
  color,
  perspective = false,
  cell,
  lineWidth = 2,
  horizon = 0.5,
  glowColor,
  fade = 0.6,
  opacity = 1,
  speed = 1,
  style,
  className,
  ...motion
}: GridProps) {
  const theme = useTheme();
  const { width, height, u } = useViewport();
  const m = useMotion(motion);
  const base = background ?? theme.colors.background;
  const seconds = (m.frame / m.fps) * speed;
  const cellPx = Math.max(u(cell ?? (perspective ? 110 : 80)), 4);
  const stroke = color ?? (perspective ? alpha(theme.colors.accent, 0.55) : theme.colors.border);
  const lineOpacity = clamp01(opacity) * m.presence;

  if (!perspective) {
    const inner = (1 - clamp01(fade)) * 90;
    const mask =
      fade > 0 ? `radial-gradient(ellipse farthest-corner at 50% 50%, black ${inner}%, transparent 100%)` : undefined;
    return (
      <AbsoluteFill className={className} style={{ background: base, ...style }}>
        <svg
          width={width}
          height={height}
          aria-hidden="true"
          style={{ position: "absolute", inset: 0, WebkitMaskImage: mask, maskImage: mask }}
        >
          <path
            d={flatPath(width, height, cellPx, seconds)}
            fill="none"
            stroke={stroke}
            strokeWidth={u(lineWidth)}
            opacity={lineOpacity}
          />
        </svg>
      </AbsoluteFill>
    );
  }

  const horizonY = height * horizon;
  const eye = height - horizonY;
  // Fog in the background color hides the densest far lines; `fade` stretches it toward the viewer.
  const fogEnd = 16 + clamp01(fade) * 44;
  const glow = alpha(glowColor ?? theme.colors.accent, 0.3 * m.presence);

  return (
    <AbsoluteFill className={className} style={{ background: base, ...style }}>
      <svg width={width} height={height} aria-hidden="true" style={{ position: "absolute", inset: 0 }}>
        <path
          d={floorPath(width, height, cellPx, horizonY, seconds)}
          fill="none"
          stroke={stroke}
          strokeWidth={u(lineWidth)}
          opacity={lineOpacity}
        />
      </svg>
      <div
        style={{
          position: "absolute",
          left: 0,
          right: 0,
          top: horizonY - 1,
          height: eye + 1,
          background: `linear-gradient(180deg, ${base} 0%, ${base} 14%, ${alpha(base, 0)} ${fogEnd}%)`,
        }}
      />
      <AbsoluteFill
        style={{
          background: `radial-gradient(ellipse ${width * 0.55}px ${eye * 0.45}px at 50% ${horizonY}px, ${glow} 0%, ${alpha(glowColor ?? theme.colors.accent, 0)} 100%)`,
        }}
      />
    </AbsoluteFill>
  );
}

On this page