--- title: "How to Make a Product Demo Video in React" author: "Remocn" published: "2026-08-01T10:38:19.068Z" updated: "2026-08-14T08:00:00.904Z" url: "https://zyvop.com/how-to-make-a-product-demo-video-in-react-ls1u6" canonical: "https://dev.to/remocn/how-to-make-a-product-demo-video-in-react-ief" tags: ["react","webdev","JavaScript","tutorial"] --- # How to Make a Product Demo Video in React ![Cover image]() This is a build log for a 30-second product demo video, written entirely in React and rendered to an `.mp4` you can drop on a landing page, post to X, or attach to a Product Hunt launch. No After Effects, no screen recorder, no dragging clips around a GUI timeline. The stack is [Remotion]() — the React framework that renders real video from components — and [remocn](), a shadcn-style registry of copy-paste animation and UI components built for it. We'll assemble three scenes: a hook line over an animated background, a simulated product walkthrough (the app UI rebuilt on a timeline instead of screen-recorded), and a closing scene with a live star counter and a call to action. Every scene is a React component, and the whole thing is one file tree you can re-render whenever your UI changes. ## Why build a demo video in code A GUI editor is faster for a one-off. A demo video is rarely a one-off: the product UI changes, the pricing changes, the headline changes, and every change means re-recording and re-cutting. When the video is React, a change is a prop edit and a re-render, and the diff lives in git next to the feature that caused it. You also skip the screen recorder entirely — instead of capturing your running app, you rebuild the parts you want to show as components, which stay crisp at any resolution and never leak a stray notification or the wrong cursor. The tradeoff is real: you write code instead of dragging clips, so this pays off when the video has a life beyond its first export. (There's a longer editor-versus-code comparison elsewhere; this article is the build.) ## Install Remotion and the components remocn assumes a Remotion project already exists. If you don't have one, `create-video` scaffolds it; if you've never run the shadcn CLI here, `init` writes a `components.json`. Then a single `add` pulls every component this article uses. ```python # 1. A Remotion project (skip if you already have one) npx create-video@latest # 2. shadcn config (skip if you already have components.json) npx shadcn@latest init # 3. Every component this article uses, in one run npx shadcn@latest add \ @remocn/shader-mesh-gradient @remocn/staggered-fade-up \ @remocn/signup-flow @remocn/backdrop \ @remocn/whip-pan @remocn/zoom-blur \ @remocn/github-stars @remocn/soft-blur-in @remocn/confetti \ @remocn/terminal-simulator ``` Enter fullscreen mode Exit fullscreen mode remocn follows shadcn's "own your code" model: `add` copies each component's TypeScript source into your project under `components/remocn/`, so you import them by path (`@/components/remocn/signup-flow`) and edit them like your own files — there's no runtime dependency to track. The registry also resolves dependency chains for you. `signup-flow` alone pulls in the remocn-ui core plus `cursor`, `input`, `button`, `toast`, `field`, and `blur-in`, and `whip-pan` installs `@remotion/transitions`, so the single command above leaves you with a working set. One licensing note, accurate at the time of writing: Remotion is source-available and free for individuals and companies of up to three people; larger companies need a paid company license. remocn itself is MIT. ## Set up the composition A Remotion video is a ``: an id, the root component to render, a frame count, a frame rate, and a resolution. We're rendering 1920×1080 at 30 fps. Thirty seconds is 900 frames, and after accounting for the two transitions the exact number lands at 906 — here's the budget the rest of the article fills in. | Scene | What's on screen | Frames | Seconds | | --- | --- | --- | --- | | 1 — Hook | shader background + `StaggeredFadeUp` headline | 150 | 5.0 | | ↳ whip-pan | overlaps scenes 1 and 2 | −26 | −0.9 | | 2 — Product | `signup-flow` simulated on a timeline | 500 | 16.7 | | ↳ zoom-blur | overlaps scenes 2 and 3 | −18 | −0.6 | | 3 — Closing | `github-stars` + CTA + `confetti` | 300 | 10.0 | | **Total** | | **906** | **30.2** | The scene sequences sum to 950 frames; the two transitions overlap their neighbors by 26 and 18 frames, so the finished video is `950 − 44 = 906` frames. `create-video` already wired `registerRoot` for you — you only edit the root: ```javascript // src/Root.tsximport { Composition } from "remotion";import { ProductDemo } from "./ProductDemo"; export const RemotionRoot = () => (); ``` Enter fullscreen mode Exit fullscreen mode `ProductDemo` is the component we build up over the next sections. Preview it live at any time with `npx remotion studio`, where you can scrub the timeline frame by frame. ## Scene 1 — the hook line The opening has one job: state the value in a few words before anyone scrolls past. We put an animated shader behind a headline that assembles word by word. Both are remocn components, and both are deterministic — the shader freezes its own internal clock and instead reads the current frame, so the motion is a pure function of the timeline rather than wall-clock time. That's what makes a Remotion render reproducible across machines and safe to render in parallel chunks. ```typescript // src/scenes/HookScene.tsximport { AbsoluteFill } from "remotion";import { ShaderMeshGradient } from "@/components/remocn/shader-mesh-gradient";import { StaggeredFadeUp } from "@/components/remocn/staggered-fade-up"; export const HookScene = () => ( ); ``` Enter fullscreen mode Exit fullscreen mode `ShaderMeshGradient` fills the frame; the second `AbsoluteFill` layers the headline on top. `StaggeredFadeUp` lifts each word up by `distance` pixels and fades it in, offsetting each next word by `staggerDelay` frames — at 4 frames a word, a four-word line finishes assembling in well under a second. Mesh gradients render bright, so the headline uses a near-black `color`; if you swap in a darker shader (there are 18 in the registry), flip the text to white. The typography components size themselves from `fontSize`, so they're resolution-independent — no wrapper needed here. ## Scene 2 — simulate the product on a timeline This is the scene most people reach for a screen recorder to make. Don't. Recording your running app captures whatever resolution your display happens to be, drags in real network lag, and risks a notification popping in mid-take. The alternative — the "fake app" technique — is to rebuild the slice of UI you want to show as a component and script it on the timeline. It renders retina-sharp at any output resolution, it re-renders when your real UI changes, and every take is identical. remocn ships these as scripted flows. `signup-flow` drives a complete account-creation sequence: a cursor moves field to field, each input fills in, the submit button steps through hover → press → loading → success, and a success toast slides in — all keyed to frame numbers, not timers. You feed it your copy through props. These scene sims are authored against a 1280×720 reference so their internal pixel math stays predictable. To drop one into a 1080p composition, scale that reference up once with a small wrapper you'll reuse for the closing scene too: ```typescript // src/components/Stage.tsximport type { ReactNode } from "react";import { AbsoluteFill } from "remotion"; export const Stage = ({ children }: { children: ReactNode }) => (
{children}
); ``` Enter fullscreen mode Exit fullscreen mode A 1280×720 box scaled by 1.5 is exactly 1920×1080, centered. Now the scene: a soft gradient behind the scaled signup card. ```javascript // src/scenes/ProductScene.tsximport { AbsoluteFill } from "remotion";import { SignupFlow } from "@/components/remocn/signup-flow";import { Stage } from "../components/Stage"; export const ProductScene = () => ( ); ``` Enter fullscreen mode Exit fullscreen mode Every string is a prop, so this becomes your product by editing the copy — no re-recording. `signup-flow`'s scripted timeline runs about 360 frames (roughly 12 seconds), which is why Scene 2 gets 500 frames in the budget: enough to play the whole flow and hold on the completed form for a beat before the transition out. If your product is a CLI rather than a web app, swap `signup-flow` for `terminal-simulator`, which types commands into a console window and rolls older lines off the top — same idea, different surface. ## Stitch the scenes with TransitionSeries Individual scenes are just components. To cut between them with motion, wrap them in `TransitionSeries` from `@remotion/transitions` and drop a `TransitionSeries.Transition` between each pair. remocn's transitions are presentation functions you hand to that element: `whipPan` flings both scenes through the frame in a single camera whip, and `zoomBlur` punches forward into the next scene with a blur. A transition doesn't add time — it overlaps the last frames of the outgoing scene with the first frames of the incoming one, so the finished duration is the sum of the sequences minus the sum of the transitions. ```typescript // src/ProductDemo.tsximport {TransitionSeries,linearTiming,springTiming,} from "@remotion/transitions";import { whipPan } from "@/components/remocn/whip-pan";import { zoomBlur } from "@/components/remocn/zoom-blur";import { HookScene } from "./scenes/HookScene";import { ProductScene } from "./scenes/ProductScene";import { ClosingScene } from "./scenes/ClosingScene"; export const ProductDemo = () => ( ); ``` Enter fullscreen mode Exit fullscreen mode The `timing` and `presentation` props are separate on purpose: `presentation` is the look of the move, `timing` is its curve and length. `whipPan` reads well on a linear ramp. `zoomBlur` applies no internal easing of its own, so its character comes entirely from the timing function — `springTiming` gives the punch-in a springy settle. Sequences sum to 950 frames, the two transitions overlap 44, and the result matches the 906 we set on the ``. `ClosingScene` is the one piece still missing; it's next. ## Scene 3 — social proof and the call to action The close does two things back to back: show that other people already use the product, then tell the viewer what to do. `github-stars` handles the first half — a fly-through of stargazers with an odometer that counts up to the total. It renders standalone from built-in sample data, or you pass real stargazers you fetched. Like the signup sim, it targets a 1280×720 reference, so it goes through the same `Stage` wrapper. Then a `Backdrop` fills the frame for the CTA: `SoftBlurIn` resolves the final line out of a blur, and `Confetti` fires on top. ```javascript // src/scenes/ClosingScene.tsximport { AbsoluteFill, Sequence } from "remotion";import { Backdrop } from "@/components/remocn/backdrop";import { Confetti } from "@/components/remocn/confetti";import { GitHubStars } from "@/components/remocn/github-stars";import { SoftBlurIn } from "@/components/remocn/soft-blur-in";import { Stage } from "../components/Stage"; const STARS_LEN = 150; export const ClosingScene = () => ( ); ``` Enter fullscreen mode Exit fullscreen mode The two halves are ordinary `` elements inside the scene: the star fly-through owns the first 150 frames, and the CTA owns the rest, starting at `from={STARS_LEN}`. `Backdrop` is remocn's background component — with `padding`, `radius`, and `shadow` zeroed out it's a full-bleed fill, and if you leave those defaults on instead it wraps the content in a rounded, shadowed floating-window frame. `Confetti`'s `startFrame` is counted from the start of its `Sequence`, so `10` means ten frames after the CTA appears. It's worth calling out why the confetti looks the same on every render: each piece comes from a seeded pseudo-random generator, never `Math.random()`, so a given `seed` produces an identical burst every time — which is exactly what a deterministic, parallelized render needs. ## Render to mp4 With the root registered, one command turns the composition into a file: ```csharp npx remotion render ProductDemo out/demo.mp4 ``` Enter fullscreen mode Exit fullscreen mode That renders `ProductDemo` at the resolution and frame count from the `` and writes `out/demo.mp4`. While you're iterating on a single scene, render just its frames instead of all 906 — `npx remotion render ProductDemo out/scene2.mp4 --frames=124-624` gives you the product scene in a fraction of the time (it starts at frame 124, since the 26-frame whip-pan overlap pulls it back from 150). When you want the full render to run somewhere other than your laptop — a CI job, or a render button inside your own app — `@remotion/renderer` drives the identical render headlessly with `selectComposition` then `renderMedia`, and Remotion Lambda splits the frames into parallel chunks in the cloud for speed. Because every component here is a pure function of the frame, all of those paths produce the same pixels. ## Where to go next The full catalog — 18 shaders, the rest of the camera and dissolve transitions, chart and chat sims, and the video-ready UI primitives that power the fake-app flows — lives at [remocn.dev/docs](). The two most useful next stops for a demo are the [transitions]() you cut with and the [UI primitives]() you compose your own product scenes from. Everything is MIT and on GitHub at [github.com/Remocn/remocn]() — a star helps if this saved you an afternoon. Swap the signup sim for your product's real flow, change the copy, and the same three-scene skeleton becomes your next launch video.