How to convert HTML to MP4 programmatically
An end-to-end recipe for turning a static HTML file into a deterministic MP4 — no timeline tool, no manual export, no headless-Chrome plumbing.
The phrase "html to mp4" sits in search analytics next to a long tail of more specific things — "html canvas to mp4", "convert webpage to video", "render dom to mp4". They all want the same thing: a way to take something that already exists in a browser and turn it into a video file that does not.
The naive recipe is screen recording. Open the page, hit record, trim, export. It works once and is unusable as a pipeline. The version that actually scales is deterministic frame capture: load the page, freeze its clock, advance it one frame at a time, encode.
What "programmatically" means here
Three properties matter:
- Reproducible. The same input produces the same output. Two renders of
intro.htmlproduce bit-identical MP4s. - Frame-accurate. No dropped frames, no jitter, no "ish" durations. A 30-second video has exactly
30 × fpsframes. - Headless. No GUI in the loop. A CI job runs the render the same way your laptop does.
requestAnimationFrame running against a wall clock breaks all three. Time has to be a parameter, not a side effect.
The render contract
The HTML side is a single function:
function render(t /* seconds */) {
// update DOM/SVG/canvas state for time t
}Everything that animates reads from t. CSS animations are out — they bind to wall time. CSS transitions are out for the same reason. SMIL is out. Anything that reads Date.now() or performance.now() is out.
What stays in: any computation you can write as a pure function of t. Easing curves, sin/cos motion, interpolated colors, springs (yes, springs can be deterministic — they are solved analytically from t, not integrated frame-by-frame), DOM updates driven by render(t).
The pipeline
A renderer that respects the contract above has four steps:
- Load. Open the HTML in headless Chromium. Wait for
hf-ready(or your equivalent — fonts loaded, images decoded, scripts executed). - Seek. Send
render(0.0),render(0.0167),render(0.0333), ... — one call per frame at the chosen fps. - Capture. After each seek, screenshot the viewport.
- Encode. Pipe the captured frames into an MP4 muxer (h.264 or h.265, your call).
That is the whole pipeline. The hard parts are not in step 1 or 4; they are in step 2 (writing animations as pure functions of t) and a footgun in step 3 (waiting for the seek to actually settle before capturing — easy to capture a stale frame).
The pitfalls
Wall-clock leaks. A single setInterval, a single transition: opacity .3s, and your output stops being deterministic. Run the render twice; diff the MP4 with ffmpeg -i a.mp4 -i b.mp4 -filter_complex psnr. If PSNR is not infinite, something is reading wall time.
Font subsetting. A font that loads halfway through render produces two visually different videos. Block render until document.fonts.ready. Better: inline the font as a data URL.
Subpixel jitter. Chromium subpixel-renders text, and which subpixels depend on rendering history. Force integer pixel positions for any element that animates.
What you get in return
When the render is deterministic, the rest of the pipeline becomes simple. CI renders the same MP4 your laptop did. Caching is easy — hash the inputs, skip the render if the output is already on disk. Diffing two versions of a video is a one-line ffmpeg invocation. None of this is true of After Effects.
The deeper writeup of why this matters is in HTML is the next video codec. The mechanics of the render path itself are in from DOM to MP4. This post is the elevator pitch for both.
A sentence to take with you
If your HTML animation is a pure function of time, your MP4 is a pure function of your HTML. Once that is true, everything downstream — CI, caching, A/B testing, batch renders — is easy. Once it is not true, nothing is.
Cite this postBibTeX · APA · Markdown
@misc{tanaka2026convert,
author = {Kira Tanaka},
title = {How to convert HTML to MP4 programmatically},
year = {2026},
url = {https://hyperframes.video/blog/html-to-mp4-programmatically},
note = {HyperFrames blog}
}Kira Tanaka. (2026, May 19). How to convert HTML to MP4 programmatically. HyperFrames. https://hyperframes.video/blog/html-to-mp4-programmatically
[How to convert HTML to MP4 programmatically](https://hyperframes.video/blog/html-to-mp4-programmatically) — 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."
Programmatic video generation in Node.js
A walkthrough of generating MP4s from a Node.js process — headless Chromium, frame capture, encoding, and the pitfalls to avoid.
Generate an MP4 from a React component
Render a React component to a deterministic MP4. The trick is treating time as a prop, not a side effect.
Recording video with Puppeteer (and what to use instead)
Puppeteer can record video — sort of. Here's the screencast API, its limits, and the deterministic alternative for real production work.
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.