@remotion/web-renderer: Allow HTML-in-Canvas API to be used#7011
@remotion/web-renderer: Allow HTML-in-Canvas API to be used#7011JonnyBurger merged 18 commits intomainfrom
@remotion/web-renderer: Allow HTML-in-Canvas API to be used#7011Conversation
…ll frames when available. When Chromium experimental API is present (chrome://flags/#canvas-draw-element), capture full composition layers by reparenting the scaffold root under a layoutsubtree canvas and using drawElementImage, then copy to OffscreenCanvas. Fall back to the existing compose() walk for partial cutouts, SVG roots, or on failure. Made-with: Cursor
…aint handler Made-with: Cursor
…ive capture - Default true; set false to force software DOM composer - Log one warning per renderStillOnWeb / renderMediaOnWeb when drawElementImage path is used - Document option on render still and render media docs pages Made-with: Cursor
- Report HtmlInCanvasLayerOutcome for full-frame captures (cutout mismatch, option off, missing drawElementImage, or drawElementImage error) - Use warn-level messages so they appear when logLevel is warn (not only on success) - Export HtmlInCanvasLayerOutcome type; document logging in web-renderer docs Made-with: Cursor
…wElementImage - Double rAF after reparenting to flush layout - One paint pass without drawing so Chromium can record layout - Retry drawElementImage on further paints when "No cached paint record" persists Made-with: Cursor
…in-canvas Instead of reparenting the scaffold div into a throwaway canvas on every frame (which never gives Chromium time to build a paint record), set up the layoutsubtree canvas once in createScaffold and keep the scaffold div as its permanent child for the lifetime of the render. - setupHtmlInCanvas(): wrapper > canvas[layoutsubtree] > div - drawWithHtmlInCanvas(): just ctx.drawElementImage() per frame — no reparenting - teardownHtmlInCanvas(): restore original DOM on dispose - createLayer: accepts HtmlInCanvasContext from scaffold, tries native draw, falls back to compose() on error - Scaffold reports why html-in-canvas is unavailable at setup time Made-with: Cursor
…html-in-canvas Two issues prevented Chromium from building a paint record for the layoutsubtree canvas children: 1. The wrapper div has visibility:hidden which is inherited by the canvas. Override with visibility:visible on the layoutCanvas — the spec says layoutsubtree children aren't visible to the user anyway, and the wrapper is already behind everything (z-index:-9999, pointer-events:none). 2. The first drawElementImage call happens synchronously after flushSync, before the browser has had any paint cycle. Add a warmup retry loop: on "No cached paint record" errors, wait for requestAnimationFrame and retry up to 10 times. After the first success, mark warmedUp=true so subsequent frames are synchronous with no overhead. Made-with: Cursor
…vas and add Studio toggle Made-with: Cursor
…etry loop for html-in-canvas Made-with: Cursor
…rawElementImage is available but fails Made-with: Cursor
…udio defaults Made-with: Cursor
@remotion/web-renderer: Use HTML-in-Canvas API for faster client-side rendering@remotion/web-renderer: Allow HTML-in-Canvas API for faster client-side rendering
…ody text Made-with: Cursor
Use singular `<Option>` component in web-renderer docs, add html-in-canvas doc page, and remove redundant warning text about pixel differences and software rasterization fallback. Made-with: Cursor
… order Made-with: Cursor
Made-with: Cursor
…der API docs Made-with: Cursor
Use HTML-in-canvas consistently in logs, Studio, CLI, and docs; refresh the HTML-in-canvas social card and articles entry; drop the Options alias from MDX; point allow-html-in-canvas at the client-side-rendering guide. Made-with: Cursor
@remotion/web-renderer: Allow HTML-in-Canvas API for faster client-side rendering@remotion/web-renderer: Allow HTML-in-Canvas API to be used
There was a problem hiding this comment.
Important
Two issues found — one bug and one robustness concern. Overall the integration is clean and well-plumbed through the stack.
1. Broken markdown link in packages/docs/docs/performance.mdx
The change on line 33 introduces a broken markdown link. The original was:
Both [`<Html5Video>`](/docs/html5-video) (formerly just called `<Video>`, from the `remotion` package) and ...The new version wraps everything in [...] without a (url):
Both [`<Html5Video>` (formerly just called `<Video>`, from the `remotion` package)] and ...This produces an unresolved reference, not a link. Should be either restored or converted to plain inline code.
2. waitForPaint in packages/web-renderer/src/html-in-canvas.ts has no timeout
Lines 89-96 — waitForPaint returns a promise that resolves only when the paint event fires. If the event never fires (plausible with an experimental API), the promise hangs forever. The outer delayRenderTimeoutInMilliseconds only applies to waitForReady, not to the capture step, so this would stall an entire render with no diagnostic.
A simple safety timeout (e.g. 5s) that reject()s would allow the existing try/catch in createLayer to catch it and fall back to the DOM composer — no render would break.
Minor: PR description says default is true
Adds
allowHtmlInCanvasoption (defaulttrue)
The code correctly defaults to false everywhere (?? false in both renderMediaOnWeb and renderStillOnWeb). The docs are also correct. Just the PR description is wrong.
Big Pickle (free) | 𝕏
Made-with: Cursor

Summary
drawElementImage,requestPaint,layoutSubtree), use it instead of the built-in DOM composer for capturing frames.allowHtmlInCanvasoption (defaulttrue) torenderMediaOnWeb()andrenderStillOnWeb()to opt out.requestPaint()+paintevent to ensure fresh paint records beforedrawElementImage, relying on Remotion's existingwaitForReadyfor frame readiness.Closes #6998
Test plan
chrome://flags/#canvas-draw-elementenabledallowHtmlInCanvas: false— confirm fallback message and software compose path is usedMade with Cursor