HTML in WebGL canvas

Rendering HTML in a WebGL canvas lets you use live DOM subtrees as dynamic GPU textures, apply shader effects to the live DOM, and integrate interactive HTML into 3D scenes.

This guide demonstrates how to render HTML into a WebGL canvas. Learn more about HTML-in-Canvas in the API overview.

Rendering HTML in a WebGL canvas requires the following steps:

  1. Prepare your canvas. Declaratively set the right attributes to let the browser know it should render HTML into a canvas. This setup is identical across 2D, WebGL, and WebGPU contexts. Learn how to set your canvas up and how to declare HTML elements to be drawn independently.
  2. Apply the HTML as a texture:
    1. Pre-allocate GPU texture storage.
    2. Upload the DOM snapshot as a texture.
  3. Synchronize the DOM and the pixels.

Draw an HTML element as a WebGL texture

In WebGL, HTML elements are uploaded to the GPU as 2D textures using texElementSubImage2D().

Pre-allocate texture storage

Unlike a 2D canvas, which manages backing memory automatically, a WebGL context requires explicit memory management.

Before uploading DOM pixels, you must pre-allocate GPU memory. This lets the browser perform fast sub-image updates without triggering expensive GPU memory re-allocations.

Pre-allocate the buffer with the selected size parameters and srcData set to null as follows:

// Only allocate texture backing once
if (!textureBufferAllocated) {
  // Pre-allocate texture backing with pre-determined size.
  gl.texImage2D(
    /* target */ gl.TEXTURE_2D,
    /* level */ 0,
    /* internalformat */ gl.RGBA8,
    /* width */ myElementWidth,
    /* height */ myElementHeight,
    /* border */ 0,
    /* format */ gl.RGBA,
    /* type */ gl.UNSIGNED_BYTE,
    /* srcData */ null
  );

  textureBufferAllocated = true;
}

For the texture to match the natural layout size of the HTML element, use canvas.getElementImageDefaultSize() to retrieve its default size in canvas grid pixels:

const { width, height } = canvas.getElementImageDefaultSize(myElement);
// Pre-allocate texture backing. Only do this once, or whenever the element
// changes size.
gl.texImage2D(
  /* target */ gl.TEXTURE_2D,
  /* level */ 0,
  /* internalformat */ gl.RGBA8,
  /* width */ Math.ceil(width),
  /* height */ Math.ceil(height),
  /* border */ 0,
  /* format */ gl.RGBA,
  /* type */ gl.UNSIGNED_BYTE,
  /* srcData */ null
);

Upload the DOM snapshot as a texture

The next step is to upload the HTML element into your texture inside your render loop or paint listener.

canvas.onpaint = () => {
  // 1. Pre-allocate texture backing first
  // ...

  // 2. Upload HTML snapshot into the WebGL texture
  gl.texElementSubImage2D(
    /* target  */ gl.TEXTURE_2D,
    /* level   */ 0,
    /* xoffset */ 0,
    /* yoffset */ 0,
    /* element */ myElement // or an ElementImage from `captureElementImage()`
  );

  // ...
};

Synchronize geometry

Unlike in 2D contexts, the browser can't automatically infer where the texture appears on screen in a 3D context. You need to align DOM hit-testing and accessibility bounding boxes with your 3D mesh manually.

To do so, construct a DOMMatrix that maps the element's border box to its rendered canvas pixel location, then apply it using updateElementGeometry():

canvas.addEventListener("paint", () => {
  // 1. Upload fresh DOM snapshot to GPU texture
  gl.bindTexture(gl.TEXTURE_2D, myTexture);
  gl.texElementSubImage2D(gl.TEXTURE_2D, 0, 0, 0, myElement);

  // 2. Draw your 3D scene
  draw3DScene();

  // 3. Construct the CSS-pixel transform mapping the element's
  // border box to its drawn position on the canvas as needed
  // (for example: scale and translate)
  const canvasTransform = new DOMMatrix();
  // scale X
  canvasTransform.a = canvas.clientWidth / myElement.offsetWidth;
  // scale Y
  canvasTransform.d = canvas.clientHeight / myElement.offsetHeight;
  // translate X (CSS px)
  canvasTransform.e = 50;
  // translate Y (CSS px)
  canvasTransform.f = 100;

  // 4. Update DOM position, accessibility focus rings, and pointer hit-testing
  canvas.updateElementGeometry(myElement, {
    canvasTransform: canvasTransform,
  });
});

Next steps