---
name: remotion-programmatic-video
description: Generate videos programmatically with Remotion (React for video). Use for data-driven video — personalized greetings, social asset generators, dashboard recap videos, product walkthroughs, animated explainers. Covers composition design, prop-driven scenes, the SSR render pipeline, and production-grade Lambda rendering.
category: video-editing
version: 0.1.0
tags: [remotion, video, react, programmatic, mp4, montage]
recommended_npm: ["@remotion/cli", "@remotion/bundler", "@remotion/renderer", "@remotion/lambda", "remotion"]
license: Remotion-Personal
author: claude-code-skills
---

Remotion turns React into a video renderer: every frame is a React render at a fixed `fps`. You design with `useCurrentFrame()` and `interpolate()`, then render headless to MP4/MOV/WebM/GIF.

## Composition skeleton

```tsx
// src/Root.tsx
import { Composition } from "remotion";
import { Greeting, type GreetingProps } from "./Greeting";

export const RemotionRoot = () => (
  <Composition
    id="greeting"
    component={Greeting}
    durationInFrames={150}
    fps={30}
    width={1920}
    height={1080}
    defaultProps={{ name: "World", accent: "#ff5722" } satisfies GreetingProps}
    schema={GreetingPropsSchema}    // Zod schema → typed renders + studio form
  />
);
```

```tsx
// src/Greeting.tsx
import { AbsoluteFill, useCurrentFrame, interpolate, spring, useVideoConfig } from "remotion";
import { z } from "zod";

export const GreetingPropsSchema = z.object({
  name: z.string().min(1).max(40),
  accent: z.string().regex(/^#[0-9a-f]{6}$/i),
});
export type GreetingProps = z.infer<typeof GreetingPropsSchema>;

export const Greeting: React.FC<GreetingProps> = ({ name, accent }) => {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const scale = spring({ frame, fps, config: { damping: 14 } });
  const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: "clamp" });

  return (
    <AbsoluteFill style={{ background: "#0a0a0a", color: accent, alignItems: "center", justifyContent: "center" }}>
      <h1 style={{ fontFamily: "Geist", fontSize: 220, transform: `scale(${scale})`, opacity }}>
        Hello, {name}
      </h1>
    </AbsoluteFill>
  );
};
```

## Render from Node (production)

```ts
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import path from "node:path";

const bundleLocation = await bundle({ entryPoint: path.resolve("./src/index.ts") });

const composition = await selectComposition({
  serveUrl: bundleLocation,
  id: "greeting",
  inputProps: { name: "Maya", accent: "#22d3ee" },
});

await renderMedia({
  composition,
  serveUrl: bundleLocation,
  codec: "h264",
  outputLocation: "./out/maya.mp4",
  inputProps: { name: "Maya", accent: "#22d3ee" },
  pixelFormat: "yuv420p",
  imageFormat: "jpeg",
  jpegQuality: 95,
  concurrency: 4,                    // tune to vCPU count
  onProgress: ({ progress }) => process.stderr.write(`\r${Math.round(progress * 100)}%`),
});
```

## Patterns that work

- **One composition per output type.** Separate `greeting`, `recap`, `social-square`. Don't conditionally swap layouts in one component.
- **Sequence-based timing.** Use `<Sequence from={30} durationInFrames={60}>` to scope `useCurrentFrame()` to a sub-clip. Composes cleanly.
- **Interpolate with `easing` from `remotion`.** `Easing.bezier(0.16, 1, 0.3, 1)` (cubic-bezier "easeOutExpo") feels premium.
- **Pre-fetch fonts and images** with `delayRender()` / `continueRender()` so frames don't render with fallback fonts.
- **Audio via `<Audio src={…} />`** with `volume={(f) => fadeOut(f)}` for ducking.

## Scaling renders

- Local box: `concurrency` ≈ vCPU count, `imageFormat: "jpeg"` for speed, `"png"` for transparency.
- Lambda: `@remotion/lambda` splits frames across functions. For a 30s 1080p30 video, use `framesPerLambda: 100` and ~9 Lambdas in parallel — finishes in ~30s.

## Anti-patterns

- ❌ Re-fetching data on every frame in `useCurrentFrame` — pre-fetch with `delayRender` or pass via `inputProps`.
- ❌ Depending on `requestAnimationFrame` or `setTimeout` — frames are deterministic; time = `frame / fps`.
- ❌ Using video CSS-only transitions (e.g. `transition: opacity 1s`) — Remotion renders frame-by-frame, browser transitions don't apply.
- ❌ Putting heavy components inside a `<Sequence>` you only show for 5 frames — Remotion still mounts them.
- ❌ Generating a 4K video at 60fps "just because" — render time scales with pixel-frames; pick the lowest spec the platform requires.

## Quality gates

- Output `pix_fmt=yuv420p` — Safari/QuickTime compatibility.
- File plays at the expected duration (durationInFrames / fps).
- No skipped frames — `onProgress` reaches 1.0 monotonically.
- Final encoding pass applies `loudnorm` (see `ffmpeg-pipeline-broadcast`) for consistent loudness.

> NOTE on licensing: Remotion is free for individuals and companies under 4 employees. Larger commercial use requires a Remotion Pro license. This skill assumes you've checked the terms.
