Skill 01 · Remotion Best Practices
Subchapter 1.64
remotion-markup/html-in-canvas.mdMarkdown4 KBView on GitHub
<HtmlInCanvas> in RemotionRenders children into a <canvas> so you can post-process them with the Canvas 2D API or WebGL.
Only works in Chrome 149+ with the chrome://flags/#canvas-draw-element flag enabled.
Give the user a notice.
Do not nest <HtmlInCanvas> components. Remotion rejects nesting because Chrome does not reliably render nested HTML-in-canvas subtrees.
If you make use of WebGL during renders, you need to enable it:
From the CLI:
npx remotion render --gl=angleSet it as the default for Studio and CLI (advised):
import { Config } from "@remotion/cli/config";
Config.setChromiumOpenGlRenderer("angle");By default, draws to canvas with no effect applied:
import { HtmlInCanvas } from "remotion";
export const MyComp = () => {
return (
<HtmlInCanvas width={1280} height={720}>
<div style={{ fontSize: 80 }}>
Hello
</div>
</HtmlInCanvas>
);
};onPaint runs whenever the content updates. Call ctx.drawElementImage(elementImage, 0, 0) to draw the captured DOM, and assign the returned transform to element.style.transform so DOM selection still aligns with the painted output.
import {
AbsoluteFill,
HtmlInCanvas,
type HtmlInCanvasOnPaint,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { useCallback } from "react";
export const Blur = () => {
const frame = useCurrentFrame();
const { width, height, fps } = useVideoConfig();
const onPaint: HtmlInCanvasOnPaint = useCallback(
({ canvas, element, elementImage }) => {
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Failed to acquire 2D context");
const blurPx = 4 + 18 * (0.5 + 0.5 * Math.sin((frame / fps) * Math.PI));
ctx.reset();
ctx.filter = `blur(${blurPx}px)`;
const transform = ctx.drawElementImage(elementImage, 0, 0);
element.style.transform = transform.toString();
},
[frame, fps],
);
return (
<HtmlInCanvas width={width} height={height} onPaint={onPaint}>
<AbsoluteFill
style={{
justifyContent: "center",
alignItems: "center",
fontSize: 120,
}}
>
<h1>
Hello
</h1>
</AbsoluteFill>
</HtmlInCanvas>
);
};For WebGL, set up the context, program, and texture in onInit and return a cleanup function. Inside onPaint, upload the captured DOM with gl.texElementImage2D(...) and draw.
const onInit: HtmlInCanvasOnInit = useCallback(({ canvas }) => {
const gl = canvas.getContext("webgl2", { alpha: true, premultipliedAlpha: true });
if (!gl) {
throw new Error(
"WebGL2 unavailable. Try rendering with the --gl=angle option. See https://remotion.dev/docs/gl-options.",
);
}
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
// compile program, create texture, set up VAO...
return () => {
// delete program, texture, buffers...
};
}, []);
const onPaint: HtmlInCanvasOnPaint = useCallback(({ elementImage }) => {
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, elementImage);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}, []);For a fully working minimal example, see https://github.com/remotion-dev/remotion/blob/main/packages/docs/components/demos/HtmlInCanvasDocsDemoWebGL.tsx (opens in a new tab).
onPaint may be async. Remotion holds the frame open via delayRender() until the promise resolves. Useful for multi-pass effects with createImageBitmap.