Skip to content
Founding Lifetime: every Pro skillfirst 100 members · $169 onceUpgrade
Lookbk
Sign In

HTML to video: 5 ways to turn HTML into an MP4

Turn HTML into an MP4: render it frame by frame in headless Chrome with FFmpeg, use HyperFrames or Remotion, record the screen, or record a canvas.

For
Developers who need an MP4 from a web page or animation
Written for
Claude CodeCodexCursorGitHub CopilotGemini CLIOpenCodeAny agent that reads SKILL.md folders
Time
10 min read · updated Nov 13, 2026

The short answer

  1. To turn HTML into a video, open the page in a browser and either record it while it plays or render it frame by frame: pause the animation, move it to each frame’s exact time, take a screenshot, and encode the screenshots into an MP4 with FFmpeg. Recording is quicker to set up; rendering frame by frame gives a smooth file on any machine, because how fast the computer draws no longer matters.
  2. You can script the frame-by-frame route yourself with Playwright and FFmpeg, or use a framework that does it: HyperFrames renders plain HTML under Apache 2.0, and Remotion renders React components under its own license. For an animation drawn on a <canvas>, captureStream() and MediaRecorder record it inside the browser.

Pick a method

Ways to turn HTML into a video
MethodYou start fromTimingFits when
A screen recorderAny page open in a browserReal timeYou need one clip now and the animation is light
Playwright or Puppeteer recordingA URL and a script of clicksReal timeYou want a recorded walk through a site, such as a test run
Your own frame-by-frame scriptAn HTML file with CSS or JavaScript animationSet for each frameYou want full control and no framework
HyperFramesHTML and CSS with a seekable animation, such as GSAP or CSSSet for each frameYou design videos as web pages, or an agent writes them
RemotionReact componentsSet for each frameYour team already works in React
captureStream() and MediaRecorderA <canvas> elementReal timeUsers export a video from your web app

Why frame-by-frame rendering matters

A recording captures the page while real time runs. Whatever the browser has drawn when a frame is sampled is what ends up in the file, so a moment that is slow to draw shows up as a stutter.

Rendering frame by frame takes real time out of it. The renderer stops the page’s clock, moves the animation to exactly 0, 1/30 and 2/30 of a second and so on, and takes a screenshot of each moment. A slow frame only takes longer to capture; it doesn’t change the video. The README of timesnap, an open-source tool built on this idea, explains it: time runs on a virtual timeline, so the page finishes its work before each screenshot, and capture can run slower than real time.

There are two clocks to control. CSS animations, CSS transitions and the Web Animations API can be paused and set to any time through document.getAnimations(). JavaScript that reads Date, timers or requestAnimationFrame needs those replaced with fakes that move only when the renderer says so.

Method 1: render frame by frame with Playwright and FFmpeg

This script needs Node.js, Playwright (npm install playwright, then npx playwright install chromium) and FFmpeg. Put it next to your index.html. It renders 5 seconds at 30 frames a second into a frames folder.

render.mjs
import { mkdirSync } from 'node:fs'
import { chromium } from 'playwright'

const FPS = 30
const SECONDS = 5
mkdirSync('frames', { recursive: true })

const browser = await chromium.launch()
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } })
// Fake the page's clock (Date, timers, requestAnimationFrame) and hold it still.
await page.clock.install({ time: 0 })
await page.clock.pauseAt(1000)
await page.goto(`file://${process.cwd()}/index.html`)
await page.evaluate(() => document.fonts.ready)

let ms = 0
for (let i = 0; i < FPS * SECONDS; i++) {
  // CSS animations and transitions: pause each one at this frame's time.
  await page.evaluate((t) => {
    for (const a of document.getAnimations()) {
      a.pause()
      a.currentTime = t
    }
  }, ms)
  await page.screenshot({ path: `frames/${String(i).padStart(4, '0')}.png` })
  // JavaScript animations: move the fake clock on to the next frame.
  const next = Math.round(((i + 1) * 1000) / FPS)
  await page.clock.runFor(next - ms)
  ms = next
}
await browser.close()

Run node render.mjs, then join the frames into an MP4:

Terminal
ffmpeg -framerate 30 -i frames/%04d.png -c:v libx264 -pix_fmt yuv420p -movflags +faststart out.mp4
  • -framerate 30 gives the frame rate you rendered at. FFmpeg’s image sequence reader assumes 25 without it, so 150 frames would play for 6 seconds instead of 5.
  • frames/%04d.png matches 0000.png, 0001.png and so on.
  • -pix_fmt yuv420p keeps 4:2:0 color, which most H.264 profiles use. In our test, FFmpeg picked 4:4:4 and the High 4:4:4 Predictive profile when the flag was left out.
  • -movflags +faststart moves the file’s index to the start, which FFmpeg’s documentation describes as better for playback.

In our test (Playwright 1.63 with Chromium 153, FFmpeg 9.0.2), a page with a CSS animation and a requestAnimationFrame animation on a <canvas> rendered to a 5-second 1920×1080 H.264 MP4. The CSS animation came out byte for byte the same in every run. The canvas animation moved smoothly, but at a given frame it sat up to about 10 milliseconds off the target time, and not by the same amount in every run. For exact JavaScript animation, use a library timeline you can seek, which is what HyperFrames does.

  • The script sets every CSS animation to the same time, which is right for animations that start when the page loads. One that starts later, on a click or a timer, needs its own start time added.
  • It waits for document.fonts.ready, which resolves once fonts have loaded and layout is done, so no frame is taken with a fallback font.
  • Neither getAnimations() nor Playwright’s clock covers a playing <video> element. timecut, a BSD-3-Clause tool built on timesnap and Puppeteer, steps video playback too, but its README warns that CSS transitions and animations will likely not render as intended.

Method 2: HyperFrames

HyperFrames is HeyGen’s open-source framework for this route, under Apache 2.0. Its README sums it up as “Write HTML. Render video. Built for agents.” You write the video as an HTML page the size of the frame, mark when each element is on screen, and register a paused animation timeline. The renderer seeks each frame in headless Chrome and encodes the result with FFmpeg, so you never write the capture loop above.

index.html (excerpt)
<div id="root" data-composition-id="main" data-start="0" data-duration="5"
     data-width="1920" data-height="1080">
  <h1 id="title" class="clip" data-start="0" data-duration="5" data-track-index="0">Ship it today.</h1>
</div>
<script>
  const tl = gsap.timeline({ paused: true })
  tl.from('#title', { y: 60, duration: 0.6, ease: 'power3.out' }, 0)
  window.__timelines['main'] = tl
</script>
Terminal
npx hyperframes init my-video
cd my-video
npx hyperframes preview
npx hyperframes render
  • It needs Node.js 22 or newer and FFmpeg.
  • Animation can come from GSAP, CSS, Lottie, Three.js, Anime.js, the Web Animations API or your own frame adapter, as long as it can be seeked.
  • In version 0.8.192, render writes MP4 by default, and also WebM, MOV, GIF, HLS or a PNG sequence. MOV and WebM keep transparency. The frame rate is --fps, else the composition’s data-fps, else 30.
  • npx hyperframes skills update installs its agent skills, so a coding agent can write compositions for you.

The HyperFrames tutorial builds, checks and renders a full composition step by step.

Method 3: Remotion

Remotion renders frame by frame too, from React. A component reads the current frame number and draws that moment, and a composition pairs the component with its width, height, durationInFrames and fps. npx remotion render renders it, using a Chrome Headless Shell that Remotion downloads into node_modules.

Check the license before you use it at work. Remotion’s license page says it is “free to use for individuals and companies up to three people,” and that a Company License is “required only for entities ineligible for the Free License.” Its LICENSE.md makes you eligible for free use as an individual, a for-profit organization with up to 3 employees or a non-profit or not-for-profit organization, or while you are “evaluating whether Remotion is a good fit, and are not yet using it in a commercial way.” HyperFrames vs Remotion compares the two.

Method 4: record the screen

When the animation is light and you need a clip now, record it. The first two options need nothing installed.

  • Mac: press Shift-Command-5 and choose to record the entire screen or a portion of it. Recordings are saved as .mov files.
  • Windows: open Snipping Tool, select Record, then New, select the area to record, then Start. Select Stop when you are done.
  • Playwright: pass recordVideo when you create a browser context. The video is saved when the context closes.
record.mjs
import { chromium } from 'playwright'

const browser = await chromium.launch()
const context = await browser.newContext({
  viewport: { width: 1280, height: 720 },
  recordVideo: { dir: 'videos/', size: { width: 1280, height: 720 } },
})
const page = await context.newPage()
await page.goto(`file://${process.cwd()}/index.html`)
await page.waitForTimeout(2500) // let the animation play in real time
await context.close() // the video is written when the context closes
await browser.close()

Playwright records at 25 frames a second unless you set fps. In our test it wrote a WebM file, and the recording began when the page opened, so it ran about a second longer than the wait. Trim the start when you convert it:

Terminal
ffmpeg -ss 1 -i recording.webm -c:v libx264 -pix_fmt yuv420p out.mp4

Puppeteer has page.record({ path: 'recording.mp4' }), which its docs say outputs MP4. They mark the older page.screencast() as obsolete.

Method 5: record a canvas with MediaRecorder

If the animation is drawn on a <canvas>, the browser can record it without screenshots. canvas.captureStream(30) returns a media stream of the canvas, and MediaRecorder writes the stream to a file. It runs in the page itself, so it suits an export button in a web app.

JavaScript
const canvas = document.querySelector('canvas')
const stream = canvas.captureStream(30)
const type = ['video/mp4;codecs=avc1', 'video/webm'].find((t) =>
  MediaRecorder.isTypeSupported(t),
)
const recorder = new MediaRecorder(stream, { mimeType: type })
const chunks = []
recorder.ondataavailable = (e) => chunks.push(e.data)
recorder.onstop = () => {
  const a = document.createElement('a')
  a.href = URL.createObjectURL(new Blob(chunks, { type }))
  a.download = type.startsWith('video/mp4') ? 'canvas.mp4' : 'canvas.webm'
  a.click()
}
recorder.start()
setTimeout(() => recorder.stop(), 5000) // record 5 seconds
  • Only the canvas is recorded: the stream holds a single canvas track, and the HTML around it isn’t in the file.
  • It records in real time, for as long as you let it run. In our tests the frame rate averaged a little over 30, not exactly 30.
  • MediaRecorder.isTypeSupported() tells you which formats the browser can write. In our test in Chromium 153, plain video/mp4 gave an MP4 with VP9 video inside, and video/mp4;codecs=avc1 gave H.264, so the snippet asks for that first.

If the HTML came from Google Web Designer

Google Web Designer converts its own documents. In Design view, choose File > Convert document type > Video..., then publish the document to get the MP4. Its help pages list the export as MP4 with H.264 video and 48 kHz audio, at 30 frames a second by default, and video ads publish only to your own file system. Video isn’t offered on Linux, and HTML documents without pages, AMPHTML ads, video ads, image ads and animated GIFs can’t be converted.

Let an agent make the video

A coding agent such as Claude Code, Codex or Cursor can write the HTML, check it and run the render. Lookbk’s video skills give the agent a finished design to fill in: a HyperFrames template, a brief whose fields a script checks, and scripts that render the MP4 on your machine.

Kinetic Title Drop: an example of what it makes
Free
video
Kinetic Title DropClaude CodeCodexCursor+3 more agents
Pixel Quest Promo: an example of what it makes
Free
video
Pixel Quest PromoClaude CodeCodexCursor+3 more agents
Cursor Walkthrough: an example of what it makes
video
Cursor WalkthroughClaude CodeCodexCursor+3 more agents
Zoom Cam Walkthrough: an example of what it makes
video
Zoom Cam WalkthroughClaude CodeCodexCursor+3 more agents
  • Kinetic Title Drop (free): a six-second 1920×1080 type-only title video from three short lines, a chip, a footer and three colors.
  • Pixel Quest Promo (free): a 20-second 8-bit promo that plays like a platform game, drawn in code, with a cue sheet for adding chiptune music.
  • Cursor Walkthrough: a 9 to 15 second walkthrough of an app interface drawn in HTML and CSS, where a cursor clicks, types and drags, cut to an original beat-synced soundtrack.
  • Zoom Cam Walkthrough: a 40 to 60 second walkthrough that looks like a polished screen recording but is rendered from a step script, using a live HTML mock of your app or screenshots of it.

Every video skill is under video skills, and How to make videos with Claude Code walks through a first render.

Common problems

  • The MP4 stutters, but the page looks smooth in the browser

    It was recorded in real time and some moments took too long to draw. Render it frame by frame instead.

  • Something still jumps in a frame-by-frame render

    It moves on a clock the renderer doesn’t control, such as a playing <video>. Drive it from the animation timeline, or step it the way timecut steps video playback.

  • The first frames show a fallback font

    Take the first screenshot after document.fonts.ready resolves, and load the fonts from files in the project so every render uses the same ones.

  • The video won’t play in some players

    Encode with -pix_fmt yuv420p. Without it, FFmpeg can keep 4:4:4 color from PNG frames, which only the High 4:4:4 Predictive H.264 profile supports.

  • The Playwright recording is small or blurry

    Set recordVideo.size to your viewport size. Without it, Playwright scales the video down to fit 800×800.

Common questions

Can I convert an HTML file to MP4 by renaming it?
No. An HTML file is text that a browser draws, and renaming it doesn’t change what is inside. Open it in a browser, then record or render it as shown above.
My HTML page plays a video. How do I get that video out?
The video is a separate file. Find the <video> element in the HTML: its src attribute, or the src of a <source> element inside it, is the address of the file.
Can the video have a transparent background?
Use a format that keeps it. HyperFrames 0.8.192 renders MOV and WebM with transparency (--format mov or --format webm), and --format png-sequence writes RGBA frames.
What frame rate should I render at?
Pick one and use it in both places: the FPS constant in the script and -framerate in the FFmpeg command. HyperFrames takes --fps, else the composition’s data-fps, else 30.

Sources

What this guide says about other companies’ tools comes from their own documentation, checked on Nov 13, 2026.

Did this guide help?

See a skill’s result before you install it.

Every skill page shows what it makes, what it needs and which agents can run it.