R RenderComp
remotion nextjs react player integration web-app

Remotion with Next.js: Embedding Programmatic Video in Web Apps

Remotion with Next.js: Embedding Programmatic Video in Web Apps

Most developers encounter Remotion through the standalone CLI: scaffold a project, write compositions in React, render to MP4. That workflow is powerful for batch pipelines and pre-produced content. But what happens when you want to embed live, interactive video previews inside a web application — where the content changes based on user input, database records, or real-time data?

That is exactly what the Remotion Player is built for, and Next.js is its natural home. This guide covers everything from installation to production-ready patterns: client-side embedding, SSR hydration traps, passing dynamic data via inputProps, triggering server-side renders from API routes, and performance tuning for video-heavy pages.


Two Ways to Use Remotion in a Next.js App

Before writing a single line of code, understand that Remotion in Next.js serves two distinct use cases, and they require completely different packages:

1. The Remotion Player (client-side preview)

@remotion/player renders your composition directly in the browser — no video file is generated. The output is a React component that plays your animation live, scrubs on demand, and accepts props dynamically. This is the right tool when you want:

  • An interactive preview before a user downloads or renders
  • A product configurator where animations update as the user edits copy or colours
  • A gallery of animated templates that play on hover
  • Any case where “video as UI” is more useful than a static file

The Player runs entirely in the browser. It has no Node.js dependency and no awareness of the Remotion CLI or renderer.

2. The Renderer + SSR APIs (server-side video generation)

@remotion/renderer exposes Node.js functions — renderMedia, renderStill, getCompositions — that produce actual video files. In a Next.js context these live in API routes or Route Handlers, not in page components. This is the right tool when you want to:

  • Generate a downloadable .mp4 on demand
  • Build a “render and email” flow
  • Produce thumbnail images server-side

Most production apps use both: the Player for instant browser previews, and an API route (or a Lambda endpoint) for final rendering when the user clicks “Export.”


Installing the Remotion Player

Add the package to your Next.js project. The version must exactly match your core remotion package:

npm install @remotion/player remotion

If you already have a Remotion composition elsewhere in the monorepo, import the version from there and pin both packages to it. Mismatched versions are the most common source of cryptic runtime errors.

You will also need React and React DOM at version 18 or later. Next.js 14+ ships with React 18 by default, so this is typically already satisfied.


Your First Player Component in a Next.js Page

Here is the minimal setup. Create a composition file first — this is standard Remotion and can live anywhere in your project:

// compositions/HelloComp.tsx
import { AbsoluteFill, useCurrentFrame } from "remotion";

type Props = {
  message: string;
};

export const HelloComp: React.FC<Props> = ({ message }) => {
  const frame = useCurrentFrame();
  return (
    <AbsoluteFill
      style={{
        backgroundColor: "#0f172a",
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <p
        style={{
          color: "white",
          fontSize: 60,
          opacity: frame / 30,
        }}
      >
        {message}
      </p>
    </AbsoluteFill>
  );
};

Now embed it in a Next.js page using the Player:

// app/demo/page.tsx  (or pages/demo.tsx for Pages Router)
"use client";

import { Player } from "@remotion/player";
import { HelloComp } from "../../compositions/HelloComp";

export default function DemoPage() {
  return (
    <main>
      <h1>Live Video Preview</h1>
      <Player
        component={HelloComp}
        inputProps={{ message: "Hello from Next.js" }}
        durationInFrames={90}
        compositionWidth={1920}
        compositionHeight={1080}
        fps={30}
        style={{ width: "100%" }}
        controls
      />
    </main>
  );
}

The "use client" directive at the top is mandatory — more on that in the next section.


Handling SSR and Hydration: The Client-Only Constraint

The Remotion Player uses browser APIs (requestAnimationFrame, OffscreenCanvas, AudioContext) that simply do not exist in Node.js. This means the Player cannot render on the server — not in SSR, not in static generation, not in React Server Components.

In Next.js App Router (v13+), all components are Server Components by default. You have two options:

Option A — Mark the file as a Client Component

Add "use client" at the top of any file that imports from @remotion/player. This is the simplest approach and works well when the entire page is interactive.

Option B — Dynamic import with ssr: false

If only part of a page needs the Player and you want the rest to be server-rendered, use Next.js dynamic imports:

// app/gallery/page.tsx
import dynamic from "next/dynamic";

const VideoPlayer = dynamic(
  () => import("../../components/VideoPlayer"),
  { ssr: false }
);

export default function GalleryPage() {
  return (
    <section>
      <h2>Template Gallery</h2>
      <VideoPlayer />
    </section>
  );
}
// components/VideoPlayer.tsx
"use client";

import { Player } from "@remotion/player";
import { MyTemplate } from "../compositions/MyTemplate";

export default function VideoPlayer() {
  return (
    <Player
      component={MyTemplate}
      inputProps={{}}
      durationInFrames={120}
      compositionWidth={1280}
      compositionHeight={720}
      fps={30}
      controls
    />
  );
}

The ssr: false option tells Next.js to skip server-side rendering for this component entirely. The page shell renders on the server; the Player is injected on the client after hydration. This avoids hydration mismatch errors and keeps your Lighthouse score clean.


Passing Data to Compositions via inputProps

The inputProps prop is where Remotion’s true power for web apps becomes apparent. Any serialisable object you pass through inputProps is available inside your composition via the useCurrentFrame sibling hook — actually via the component props themselves.

This enables fully dynamic video content driven by user input or database state:

"use client";

import { useState } from "react";
import { Player } from "@remotion/player";
import { SalesCardComp } from "../../compositions/SalesCardComp";

export default function SalesCardEditor() {
  const [title, setTitle] = useState("Q2 Revenue");
  const [value, setValue] = useState("$1.2M");
  const [growth, setGrowth] = useState("+18%");

  return (
    <div style={{ display: "flex", gap: 32 }}>
      <div>
        <label>Title<br />
          <input value={title} onChange={(e) => setTitle(e.target.value)} />
        </label>
        <label>Value<br />
          <input value={value} onChange={(e) => setValue(e.target.value)} />
        </label>
        <label>Growth<br />
          <input value={growth} onChange={(e) => setGrowth(e.target.value)} />
        </label>
      </div>
      <Player
        component={SalesCardComp}
        inputProps={{ title, value, growth }}
        durationInFrames={90}
        compositionWidth={1080}
        compositionHeight={1080}
        fps={30}
        style={{ width: 400 }}
        controls
        autoPlay
        loop
      />
    </div>
  );
}

Every keystroke updates the Player in real time. The composition re-renders as a React component, so state flows naturally through the React tree. There is no serialisation overhead, no video file written to disk, and no render queue involved.

For server-driven data, fetch it in a Server Component and pass it as a serialised prop into the client wrapper:

// app/reports/[id]/page.tsx  (Server Component)
import ReportPlayer from "./ReportPlayer";

async function getReport(id: string) {
  const res = await fetch(`https://api.example.com/reports/${id}`);
  return res.json();
}

export default async function ReportPage({ params }: { params: { id: string } }) {
  const report = await getReport(params.id);
  return <ReportPlayer reportData={report} />;
}
// app/reports/[id]/ReportPlayer.tsx  (Client Component)
"use client";
import { Player } from "@remotion/player";
import { ReportComp } from "../../../compositions/ReportComp";

export default function ReportPlayer({ reportData }: { reportData: any }) {
  return (
    <Player
      component={ReportComp}
      inputProps={reportData}
      durationInFrames={180}
      compositionWidth={1920}
      compositionHeight={1080}
      fps={30}
      controls
    />
  );
}

Triggering Server-Side Renders via Next.js API Routes

The browser preview is great, but users will eventually want a file they can download, share, or upload to social media. That requires a real render — and real renders happen on the server.

There is one critical constraint you must understand before writing API route code:

You cannot use @remotion/bundler inside a Next.js API route. @remotion/bundler includes Webpack, and you cannot bundle Webpack with Webpack (which is what Next.js uses internally). Attempting this will fail at build time.

The solution is to pre-bundle your compositions before starting the Next.js server, store the bundle in a known location, and reference that bundle path from your API routes.

Step 1 — Pre-bundle your compositions

Create a standalone build script:

// scripts/bundle.ts
import { bundle } from "@remotion/bundler";
import path from "path";

const bundleLocation = await bundle({
  entryPoint: path.resolve("./compositions/index.ts"),
  webpackOverride: (config) => config,
});

console.log("Bundle written to:", bundleLocation);
// Write this path to a file or env variable your API route can read

Run this script once before deploying (or as part of your build pipeline), and save the output path in an environment variable:

npx ts-node scripts/bundle.ts
# Set REMOTION_BUNDLE_PATH=/tmp/remotion-bundle-abc123 in your environment

Step 2 — Create the API Route

// app/api/render/route.ts  (App Router)
import { renderMedia, getCompositions } from "@remotion/renderer";
import { NextResponse } from "next/server";
import path from "path";
import os from "os";

const bundlePath = process.env.REMOTION_BUNDLE_PATH!;

export async function POST(request: Request) {
  const body = await request.json();
  const { compositionId, inputProps } = body;

  const compositions = await getCompositions(bundlePath);
  const composition = compositions.find((c) => c.id === compositionId);

  if (!composition) {
    return NextResponse.json({ error: "Composition not found" }, { status: 404 });
  }

  const outputPath = path.join(os.tmpdir(), `render-${Date.now()}.mp4`);

  await renderMedia({
    composition,
    serveUrl: bundlePath,
    codec: "h264",
    outputLocation: outputPath,
    inputProps,
  });

  // In production: upload outputPath to S3/R2 and return a signed URL
  return NextResponse.json({ outputPath });
}

For production at scale, replace local renderMedia with renderMediaOnLambda from @remotion/lambda. The API route becomes a thin dispatcher that queues a Lambda invocation, polling for completion via getRenderProgress.


Template galleries are one of the most compelling use cases for remotion nextjs integration. Instead of showing static screenshots of your templates, you can play them live.

A practical pattern is to render each template in a small, muted, auto-playing loop:

"use client";

import { Player } from "@remotion/player";

type Template = {
  id: string;
  component: React.FC<any>;
  defaultProps: Record<string, unknown>;
  durationInFrames: number;
};

export function TemplateCard({ template }: { template: Template }) {
  return (
    <div className="template-card">
      <Player
        component={template.component}
        inputProps={template.defaultProps}
        durationInFrames={template.durationInFrames}
        compositionWidth={1920}
        compositionHeight={1080}
        fps={30}
        style={{ width: "100%", aspectRatio: "16/9" }}
        autoPlay
        loop
        muted
        showVolumeControls={false}
        controls={false}
      />
      <div className="template-meta">
        <span>{template.id}</span>
      </div>
    </div>
  );
}

Keep compositionWidth and compositionHeight at the true composition dimensions — the Player scales via CSS. This preserves sharpness and avoids pixel-doubling artefacts on high-DPI displays.


Performance Considerations

Embedding Remotion Players is not free. Each Player instance runs a requestAnimationFrame loop and may decode media assets. On a gallery page with dozens of templates, this adds up fast.

Lazy load off-screen Players

Use the Intersection Observer API (or a React hook wrapping it) to mount Players only when they enter the viewport:

"use client";

import { useRef, useState, useEffect } from "react";
import dynamic from "next/dynamic";

const Player = dynamic(() => import("@remotion/player").then(m => m.Player), { ssr: false });

export function LazyPlayer({ component, inputProps, ...rest }: any) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => { if (entry.isIntersecting) setVisible(true); },
      { rootMargin: "200px" }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={ref} style={{ aspectRatio: "16/9", background: "#000" }}>
      {visible && <Player component={component} inputProps={inputProps} {...rest} />}
    </div>
  );
}

Code-split your compositions

Each composition can be a separate dynamic import. Webpack (or Turbopack) will create separate chunks, so visitors only download the composition code for templates they actually view.

const SalesComp = dynamic(() =>
  import("../compositions/SalesComp").then((m) => m.SalesComp)
);

Pause off-screen Players

Use the Player’s imperative API — accessible via a ref — to pause Players when they scroll out of view, reducing CPU and battery drain:

import { PlayerRef } from "@remotion/player";
const playerRef = useRef<PlayerRef>(null);
// on IntersectionObserver exit:
playerRef.current?.pause();

FAQ

Does the Remotion Player work with Next.js App Router and React Server Components?

Yes, but only as a Client Component. Mark the wrapping file with "use client" or use dynamic(() => import(...), { ssr: false }). You cannot render the Player in a Server Component or during SSR.

Can I use @remotion/renderer inside a Next.js API route?

Yes — renderMedia, getCompositions, and renderStill work in API routes. The constraint is @remotion/bundler: it cannot run inside an API route because it embeds Webpack. Pre-bundle your compositions in a build step and pass the resulting path to the renderer.

How do I handle loading states while the Player initialises?

Wrap the dynamic import in a Suspense boundary or provide a placeholder in the loading prop of dynamic(). The Player itself does not expose a loading callback, so a CSS-based skeleton on the container div is the most practical approach.

Can the Remotion Player render audio?

Yes. Remotion’s <Audio> component and audio sequences work fully in the Player. By default, browsers block autoplay with audio — use the muted prop for gallery thumbnails and let users unmute on interaction.

What is the difference between compositionWidth/compositionHeight and the CSS style prop on the Player?

compositionWidth and compositionHeight define the intrinsic resolution of your composition — the pixel dimensions your React layout is designed for. The CSS style prop controls how large the Player element appears on the page. The Player scales the canvas to fit the container, so you can design at 1920×1080 and display at 640×360 without any code changes.

How do I trigger a real render and return a download link to the user?

Create a POST API route that calls renderMedia (or renderMediaOnLambda). Return a job ID immediately, poll for progress with a separate GET route, and when complete return a signed URL from your storage provider (S3, Cloudflare R2, etc.).

Can I use Remotion Player inside Next.js Middleware?

No. Middleware runs in the Edge Runtime, which is a restricted environment. The Player is browser-only; @remotion/renderer requires Node.js. Neither is compatible with Edge.


Building Video-First Web Apps with RenderComp Templates

The patterns in this guide are production-tested — but writing compositions from scratch takes time. RenderComp’s template library gives you a head start: every template exports a typed React component that slots directly into the component prop of your Player, with a documented inputProps interface you can wire up to your own data.

Browse templates at rendercomp.com to find the right starting point for your next.js programmatic video project. Whether you are building a personalised video generator, an animated dashboard, or a social content tool, there is a template designed for exactly that use case — and it works the same day you install it.


Published by RenderComp — Remotion animation templates for production teams.

Now available

Get 1,000+ Remotion Templates

Pay once — no subscription. Lifetime updates. TypeScript-first.

View pricing →