Render a React component to MP4 (the practical way)
Turn any React component into a deterministic MP4. Frame-pinned timing, props as variables, the export pipeline. No headless Chrome scripting required.
The question comes up about once a month: "I have a React component that animates. How do I turn it into an MP4?" The answers on Stack Overflow point to Puppeteer screen-recording, which works for a demo and falls apart in production. There's a better path that respects what React is good at and treats video as a deterministic frame sequence instead of a screen recording.
This is the engineering walk-through: deterministic time, props as variables, the difference between "captured" and "rendered" video, and where each approach stops working.
The fundamental problem with screen recording
The naive approach: spin up a headless browser, navigate to a React app, start a video recorder, wait for the animation to finish, stop the recorder. This works for demos and breaks in production for three reasons:
- Real-time playback ties recording to wall-clock. A 10-second animation takes 10 seconds to record. A 1000-variant batch takes nearly 3 hours.
- Frame timing is non-deterministic.
requestAnimationFrameruns when the browser feels like it. Two recordings of the same animation will not be byte-identical. - Dropped frames at jitter spikes. Any GC pause or system load shows up as a stuttered moment in the output.
The fix: don't record, render frame-by-frame. Drive the React component with an explicit time variable, snapshot each frame, encode the sequence into MP4. Wall-clock time disappears; the only thing that matters is "given t=0.42s, what does the component look like."
The deterministic-time prop
Replace useEffect-driven animation with a time prop:
// Don't:
function BadCounter({ target }) {
const [n, setN] = useState(0);
useEffect(() => {
const start = performance.now();
const tick = (now) => {
const t = Math.min(1, (now - start) / 1000);
setN(Math.round(target * easeOut(t)));
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}, [target]);
return <span>{n}</span>;
}
// Do:
function GoodCounter({ target, time, duration = 1 }: { target: number; time: number; duration?: number }) {
const t = Math.min(1, time / duration);
const n = Math.round(target * easeOut(t));
return <span className="tabular-nums">{n}</span>;
}The time prop is the render time in seconds. The render loop sets it explicitly for each frame: 0.000, 0.033, 0.066, 0.100, ... (at 30fps). The component is pure given a time value.
The render loop
Conceptually:
const FPS = 30;
const DURATION_S = 8;
const TOTAL_FRAMES = FPS * DURATION_S;
for (let i = 0; i < TOTAL_FRAMES; i++) {
const t = i / FPS;
await renderFrame(<MyComponent time={t} {...props} />);
}
await encodeFrames();renderFrame snapshots the rasterized React output to a PNG. encodeFrames muxes the PNG sequence into an MP4. In the HyperFrames pipeline, both are handled — you pass a component and a duration, you get an MP4.
Props as variables
Once a component has time plus its data props, the same component renders any number of variants. Pass target=1000 for one render, target=42 for the next; same component code, two different outputs.
This is the bridge to batch rendering from CSV — each CSV row is a prop object, each render is one MP4, the React code never changes.
function CounterFrame({ time, target, label }: {
time: number; target: number; label: string;
}) {
const t = Math.min(1, time / 1.5);
const eased = 1 - Math.pow(1 - t, 3);
const n = Math.round(target * eased);
return (
<div className="frame">
<div className="num tabular-nums">
{n.toLocaleString()}
</div>
<div className="label">{label}</div>
</div>
);
}Hooks that work, hooks that don't
The reframe: any hook that depends on time or props is fine. Any hook that depends on wall-clock time is not.
| Hook | Render-safe? | Notes |
|---|---|---|
useState | Yes | Initialize from props |
useMemo | Yes | Pure derivation |
useReducer | Yes | Deterministic state machine |
useEffect with empty deps | Sometimes | OK for one-time setup, but no setInterval |
useEffect reading Date.now() | No | Replace with time prop |
requestAnimationFrame | No | Render loop drives time, not rAF |
useTransition | No | Concurrent rendering is non-deterministic |
The general rule: if a component's output depends on anything other than its props, refactor.
Server-side rendering vs. headless browser
Two approaches to the snapshot step:
- Server-side render with
ReactDOMServer.renderToString→ DOM string → headless browser rasterizes. Faster per-frame, but you need the browser anyway for layout. - Render in a headless browser directly with the component re-rendered per frame. Slower per-frame, but you skip the SSR roundtrip and animations like
transformwork natively.
HyperFrames uses the second approach with a long-lived browser process (start once, render N frames, exit). The browser stays warm; only the page navigation and frame capture happen per render.
When this approach stops working
Three cases where you reach for something else:
- Real-time streaming with React. Use a WebRTC pipeline, not a render pipeline. Different problem entirely.
- Components that depend on a backend API. Pre-fetch the data, pass as props. Don't let the render loop wait on HTTP.
- Components that use Canvas or WebGL. These work, but you lose the SVG/DOM scrubbing model. Capture works fine; deterministic re-render across worker boundaries gets trickier.
For everything else — UI animations, dashboards, data-driven graphics, social posts — pure-props React renders cleanly to MP4. The mental model is "video is a function of time and props." Hold that line and the pipeline becomes mechanical.
Open the playground, paste a counter, scrub the timeline.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026render,
author = {Kira Tanaka},
title = {Render a React component to MP4 (the practical way)},
year = {2026},
url = {https://hyperframes.video/blog/react-to-mp4-tutorial},
note = {HyperFrames blog}
}Kira Tanaka. (2026, April 28). Render a React component to MP4 (the practical way). HyperFrames. https://hyperframes.video/blog/react-to-mp4-tutorial
[Render a React component to MP4 (the practical way)](https://hyperframes.video/blog/react-to-mp4-tutorial) — Kira Tanaka, 2026
Kira works on the render core: headless Chromium scheduling, frame capture, and the encoder pipeline. She cares about reproducible builds and small numbers next to the word "variance."
Animated flight itinerary cards in HTML
Build an animated boarding pass video: a plane traces an arc from origin to destination, gate and time settle in, the pass folds out — rendered as MP4.
Animated poll results as MP4
Build a poll results animation that renders to deterministic MP4: four horizontal bars race to their final percentages, then a winner ribbon sweeps in.
Render an app splash screen to MP4
How to design and render an app splash screen as a deterministic MP4 — spring-scaled monogram, fading wordmark, and a sheen sweep. Ship a real splash screen mp4 in an hour.
Building with HyperFrames? Come hang out.
We're on GitHub, in Discord, and the playground is one click away. Bring weird ideas — we collect them.