← portfolio | Blog | HTML in Canvas: When the Web Gets Weird in the Best Way
Web APIsCanvasHTMLWebGLExperimentalFrontend

HTML in Canvas: When the Web Gets Weird in the Best Way

May 17, 2026 · 7 min read

There’s a Chrome experiment quietly sitting behind a flag that, once you flip it, makes you feel like the web just got superpowers. It’s called HTML in Canvas, and the concept is simple enough to fit in a sentence: take real, semantic HTML elements and render them as pixels inside a <canvas> — while keeping them fully interactive.

That sentence sounds mundane until you realise what it unlocks.

The Missing Piece

For years, canvas and the DOM have lived in separate universes. Canvas gives you a raw bitmap — fast, flexible, GPU-friendly — but completely opaque to the browser’s layout engine. The DOM gives you accessibility, text reflow, form inputs, focus management — but visual effects beyond CSS are painful to apply.

Every time someone has wanted to do something like “apply a GLSL shader to rendered text” or “distort a login form like a liquid surface”, they’ve hit the same wall: you can’t get pixels from the DOM, and canvas doesn’t understand HTML.

drawElementImage() is the bridge.

How It Works

Enable the flag in Chrome 146+ via chrome://flags/#canvas-draw-element, then opt your HTML into the system with the layoutsubtree attribute:

<canvas id="c" style="width: 600px; height: 300px;">
  <div layoutsubtree id="content">
    <form>
      <input type="text" placeholder="Type something…" />
      <button>Submit</button>
    </form>
  </div>
</canvas>

Then in JavaScript:

const canvas = document.getElementById("c");
const ctx    = canvas.getContext("2d");

canvas.requestPaint();
canvas.addEventListener("paint", () => {
  ctx.reset();
  ctx.drawElementImage(document.getElementById("content"), 0, 0);
});

That’s it. Your form — with real focus states, real keyboard input, real browser-native rendering — is now on the canvas. And because it’s on the canvas, you can follow it up with getImageData() and go wild with the pixel array.

The Thing About Size

This tripped me up immediately. Canvas doesn’t behave like a block element — it has an intrinsic size (width/height attributes) that is completely separate from its CSS size (style.width/style.height). If these two don’t agree, your drawing surface is scaled incorrectly and everything looks blurry or clipped.

The fix is a ResizeObserver that keeps them in sync:

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    const { inlineSize: w, blockSize: h } = entry.contentBoxSize[0];
    canvas.width  = w * devicePixelRatio;
    canvas.height = h * devicePixelRatio;
    ctx.scale(devicePixelRatio, devicePixelRatio);
  }
});
observer.observe(canvas);

This is not optional. Skipping it means whatever cool effect you’re building will look terrible on a high-DPI display. Always match the drawing surface to the CSS layout size — and always account for devicePixelRatio.

Pixel Manipulation: The Fun Part

Once your HTML is on the canvas, you can iterate its pixels:

const { data, width, height } = ctx.getImageData(0, 0, canvas.width, canvas.height);

for (let i = 0; i < data.length; i += 4) {
  const r = data[i], g = data[i + 1], b = data[i + 2];
  // shift hue, apply threshold, do whatever
  data[i]     = 255 - r;
  data[i + 1] = 255 - g;
  data[i + 2] = 255 - b;
}

ctx.putImageData(new ImageData(data, width, height), 0, 0);

Combine this with requestAnimationFrame and a time variable and you get animated, per-pixel effects — applied to real, interactive HTML. Ripple effects on forms. Heat-shimmer on text. Chromatic aberration on buttons as you hover them. Things that were basically impossible to do cleanly before.

Going Further with WebGL

The 2D canvas API is great for quick experiments, but if you want real GPU muscle, you can hook this into WebGL:

const gl = canvas.getContext("webgl2");

// ... set up vertex/fragment shaders, geometry ...

// The key call: upload the HTML element as a GPU texture
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE,
  document.getElementById("content"));

gl.drawArrays(gl.TRIANGLES, 0, 6);

gl.texElementImage2D mirrors the familiar gl.texImage2D but takes a DOM element instead of an image. From here, your vertex and fragment shaders have full access to the rendered HTML as a texture, opening the door to: displacement mapping, real-time blurs, reflection effects, 3D transforms — all applied to semantically correct, accessible HTML.

Transforms and Hit Testing

One subtle detail: drawElementImage() returns a transform matrix. You should use this to synchronise the canvas coordinate system with DOM coordinate space, especially if you’re layering interactive elements back on top of canvas-rendered content.

If you skip this synchronisation, click targets won’t align with what’s visually displayed — users will click “through” your beautiful distortion effect and nothing will happen.

My Take

I’ve been building UIs for a decade — trading platforms, e-commerce flows, design systems — and I’ve always found the canvas/DOM boundary frustrating. This API feels like the beginning of something genuinely different.

The obvious applications are creative: portfolio effects, immersive landing pages, data visualisations that blend text and graphics. But the less obvious ones are more interesting to me: accessibility-preserving effects (the DOM semantics survive), form animations that don’t break tab order, canvas-native UI components that still work with screen readers.

It’s experimental, Chromium-only for now, and the API surface will change. But the concept is sound and the demos are already compelling. Worth adding to your lab environment and playing with over a weekend.

The web was starting to feel like it had reached a plateau. This is a reminder that there’s still unexplored territory.


Inspired by the original experiments from Frontend Masters Blog.

All posts