{
    "archive_path": "archive/1792750005.575665",
    "base_url": "developer.chrome.com/blog/html-in-canvas-origin-trial",
    "basename": "html-in-canvas-origin-trial",
    "bookmarked_date": "2026-10-23 10:06",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/developer.chrome.com/blog/html-in-canvas-origin-trial",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=developer.chrome.com",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": "developer.chrome.com/blog/html-in-canvas-origin-trial.html"
    },
    "domain": "developer.chrome.com",
    "extension": "",
    "hash": "MR9PWHN08GJQTR15DKTD",
    "history": {
        "archive_org": [],
        "dom": [],
        "favicon": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=developer.chrome.com"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-23T10:09:26.770576+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:26.128192+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-23T10:09:27.553193+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:26.782432+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [],
        "media": [],
        "mercury": [
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-10-23T10:09:42.002812+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:36.074794+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmp__216ec0",
                    "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-10-23T10:09:36.062979+00:00",
                "index_texts": [
                    "For years, web developers have had to make a tough architectural choice when building complex, highly-interactive visual applications on the web/ Should you lean on the DOM for its rich semantic features or render directly to the <canvas> element for low-level graphics performance?\n\nWith the new experimental HTML-in-Canvas API\u2014available now in origin trial\u2014you don't have to choose. This API lets you draw DOM content directly into a 2D canvas or a WebGL/WebGPU texture while keeping the UI interactable, accessible, and hooked up to your favorite browser features. By combining HTML with low-level graphics processing, you can create experiences that were previously impossible.\n\n\n\nThe DOM versus Canvas\n\nTo understand the power of this new API, it helps to look at the relative strengths of both the DOM and the Canvas.\n\nThe DOM is the staple of web UI. It offers text layout solutions out of the box, using semantically understood content to create rich interfaces. This lets users perform common operations across web pages seamlessly\u2014things we often take for granted, like highlighting text to copy, or right-clicking an image to save it. The DOM also integrates with essential browser features: accessibility tools, translate, find-in-page, reader mode, extensions, dark mode, browser zoom, and autofill.\n\nCanvas (and WebGL/WebGPU), on the other hand, allows for low-level access to drive a grid of pixels for highly advanced 2D and 3D graphics. Games and complex web apps (like Google Docs or Figma) require this performant, low-level access. Because the canvas is fundamentally a grid of pixels, supporting features like responsive text used to require complex custom UI logic, drastically increasing your bundle size. Crucially, all the powerful browser features integrated into the DOM break completely when the UI is trapped inside a static canvas pixel grid.\n\nThe advantages of bringing the DOM to Canvas\n\nThe HTML-in-Canvas API is the bridge that gives you the best of both worlds. By placing HTML inside the <canvas> element and synchronizing its transform, you ensure the content remains fully interactive, and that all browser integrations function automatically.\n\nHere's what you get by letting the DOM handle your UI inside a <canvas> element:\n\n\nText layout and formatting: Simplified text layout and formatting, including multiline or bidirectional text with CSS styles applied.\nForm controls: Expressive and easier to use form controls with extensive customization options.\nText selection, copy/paste, and right-click: Users can highlight text inside your 3D scenes, or right-click context menus natively.\nAccessibility: Content rendered inside the canvas is exposed to the accessibility tree. Accessibility systems can parse the UI as they do normal HTML, and expose it to systems like screen readers.\nFind-in-page: Users can use find-in-page (Ctrl/Cmd+F) to search for text, and the browser highlights it directly within your WebGL textures.\nIndexability and AI agent interfaceable: Web crawlers and AI agents can seamlessly index and read the text rendered into your 2D and 3D scenes.\nExtension integration: Browser extensions work natively. For example, a text-replacement extension automatically updates the text rendered on your 3D meshes.\nDevTools integration: You can inspect your canvas content, including for WebGL/WebGPU UI elements directly in Chrome DevTools. Tweak a CSS style in the inspector, and watch it instantly update on the 3D texture!\n\n\nHigh-level use cases\n\nThis API unlocks incredible potential across several domains:\n\n\nLarge canvas-based applications: Heavyweight web apps can now render complex application UI components natively into their canvas-driven workspaces, improving accessibility and reducing bundle weight.\n3D scenes and games: Marketing sites, immersive WebXR experiences, and web games can now place fully interactable web UI into 3D scenes\u2014like a 3D book that uses real DOM text, or an in-game terminal that natively supports copying and pasting.\n\n\nHow to use the API\n\nUsing the API happens in three phases: Setting up your canvas, rendering into the canvas, and updating the CSS transform so the browser knows where the element physically sits on the screen.\n\nPrerequisites\n\nThe HTML-in-Canvas API is in origin trial in Chrome 148 through 150. To test it on your site, use Chrome Canary 149 or later with the chrome://flags/#canvas-draw-element flag enabled. To enable the API for other users, register for the Origin Trial.\n\nStep 1: Basic Canvas setup\n\nFirst, add the layoutsubtree attribute to your <canvas> tag. This makes the browser aware of the content nested inside the canvas, preparing it to be displayed inside the canvas, and exposing it to accessibility trees.\n<canvas id=\"canvas\" style=\"width: 200px; height: 200px;\" layoutsubtree>\n  <div id=\"form_element\">\n    <label for=\"name\">Name:</label> <input id=\"name\" type=\"text\">\n  </div>\n</canvas>\n\nSize the canvas grid\n\nTo avoid blurriness of the rendered content, make sure to size the canvas grid to match the device scale factor.\nconst observer = new ResizeObserver(([entry]) => {\n  const dpc = entry.devicePixelContentBoxSize;\n  canvas.width = dpc ? dpc[0].inlineSize : Math.round(entry.contentRect.width * window.devicePixelRatio);\n  canvas.height = dpc ? dpc[0].blockSize : Math.round(entry.contentRect.height * window.devicePixelRatio);\n});\n\nconst supportsDevicePixelContentBox =\n  typeof ResizeObserverEntry !== 'undefined' &&\n  'devicePixelContentBoxSize' in ResizeObserverEntry.prototype;\nconst options = supportsDevicePixelContentBox ? { box: 'device-pixel-content-box' } : {};\nobserver.observe(canvas, options);\n\nStep 2: Rendering\n\nFor a 2D context, use the drawElementImage method. Do this inside the paint event, which triggers whenever the element redraws\u2014for example, during text highlighting or user input. It's crucial to update the element's CSS transform with the return value so interactivity continues to work.\nconst ctx = document.getElementById('canvas').getContext('2d');\nconst form_element = document.getElementById('form_element');\nconst canvas = document.getElementById('canvas');\n\ncanvas.onpaint = () => {\n  ctx.reset();\n\n  // Draw the form element at x:0, y:0\n  let transform = ctx.drawElementImage(form_element, 0, 0);\n\n  // Use the transform returned later on...\n};\n\nRender with WebGL\n\nFor WebGL, you use texElementImage2D. It functions similar to texImage2D, but takes the DOM element as the source.\ncanvas.onpaint = () => {\n  if (gl.texElementImage2D) {\n    gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, form_element);\n  }\n};\n\nRender with WebGPU\n\nWebGPU uses the copyElementImageToTexture method on the device queue, analogous to copyExternalImageToTexture:\ncanvas.onpaint = () => {\n  root.device.queue.copyElementImageToTexture(\n    valueElement,\n    { texture: targetTexture }\n  );\n};\n\nStep 3: Update the CSS transform\n\nNow that you've rendered the element into the canvas, you have to update the browser on where it's located. This ensures spatial synchronization between the canvas and the DOM's layout. This is important so that the browser can correctly map the event zone\u2014such where exactly the user clicks or hovers\u2014with where the element is rendered.\n\nFor the 2D context case, apply the transform returned by the rendering call to the .style.transform property:\nconst ctx = document.getElementById('canvas').getContext('2d');\nconst form_element = document.getElementById('form_element');\nconst canvas = document.getElementById('canvas');\n\ncanvas.onpaint = () => {\n  ctx.reset();\n  // Draw the form element at x:0, y:0\n  let transform = ctx.drawElementImage(form_element, 0, 0);\n\n  // Sync the DOM location with the drawn location\n  form_element.style.transform = transform.toString();\n};\n\nWith WebGL or WebGPU, the on-screen location of an element depends on how the output texture is used by shader code, and can't be deduced from the canvas rendering context. However, if your shader program uses a typical model view projection to draw the texture, then you can use the new convenience function element.getElementTransform() to compute a transform that can be used in the same way as the return value from drawElementImage(). To facilitate this, you need to do the following:\n\n\nConvert WebGL MVP Matrix to DOM Matrix.\nNormalize the HTML element. HTML elements are sized in pixels (for example, 200px wide). WebGL, however, usually treats objects as \"unit squares\", for example, ranging from 0 to 1. If you don't normalize, your 200px button will look 200 times larger.\nMap to the canvas viewport. This step is the \"rescaling\" phase. It stretches that unit-space math back out to match the actual pixel dimensions of your <canvas> element on the screen. It also flips the Y-axis, because in WebGL, up is positive, but in CSS, down is positive.\nCalculate the final transform. Multiply the matrices in order: Viewport * MVP * Normalization. Combining them into one final transform produces a \"map\" that tells the browser exactly where that HTML element layer should sit to align with the 3D drawing.\nApply the transform to the HTML element. This moves the HTML element layer to sit directly on top of its rendered pixels. This ensures that when a user clicks a button or selects text, they're hitting the real HTML element.\n\nif (canvas.getElementTransform) {\n  // 1. Convert WebGL MVP Matrix to DOM Matrix\n  const mvpDOM = new DOMMatrix(Array.from(htmlElementMVP));\n\n  // 2. Normalize the HTML element (pixels -> 1x1 unit square)\n  const width = targetHTMLElement.offsetWidth;\n  const height = targetHTMLElement.offsetHeight;\n\n  const cssToUnitSpace = new DOMMatrix()\n    .scale(1 / width, -1 / height, 1) // Shrink to unit size and flip Y\n    .translate(-width / 2, -height / 2); // Center the element\n\n  // 3. Map to the canvas viewport\n  const clipToCanvasViewport = new DOMMatrix()\n    .translate(canvas.width / 2, canvas.height / 2) // Move origin to center\n    .scale(canvas.width / 2, -canvas.height / 2, 1); // Stretch to canvas dimensions\n\n  // 4. Multiply: (Clip -> Pixels) * (MVP) * (pixels -> unit square)\n  const screenSpaceTransform = clipToCanvasViewport\n      .multiply(mvpDOM)\n      .multiply(cssToUnitSpace);\n\n  // 5. Apply to the transform\n  const computedTransform = canvas.getElementTransform(targetHTMLElement, screenSpaceTransform);\n  if (computedTransform) {\n    targetHTMLElement.style.transform = computedTransform.toString();\n  }\n}\n\nLibrary and framework support\n\nSome of the popular libraries have already shipped support for the HTML-in-Canvas feature.\n\nThree.js\n\nUpdating matrixes manually can be tedious, which is why frameworks are already jumping on board. Three.js has experimental support using the new THREE.HTMLTexture:\nconst material = new THREE.MeshBasicMaterial();\nmaterial.map = new THREE.HTMLTexture(uiElement); // Pass the DOM element\n\nconst geometry = new THREE.BoxGeometry(1, 1, 1);\nconst mesh = new THREE.Mesh(geometry, material);\nscene.add(mesh);\n\nPlayCanvas\n\nPlayCanvas also supports HTML-in-Canvas using their texture API:\n// Wait for the 'paint' event to set the source\ncanvas.addEventListener('paint', () => {\n    htmlTexture.setSource(htmlElement);\n}, { once: true });\ncanvas.requestPaint();\n\n// Keep up to date\ncanvas.addEventListener('paint', onPaintUpload);\n\nconst material = new pc.StandardMaterial();\nmaterial.diffuseMap = htmlTexture;\nmaterial.update();\n\nDemos\n\nBefore trying out the demos, ensure your environment is\nproperly configured.\n\nThere are several demos that serve as a reference for using the API. We are already seeing creative solutions from the community, ranging from translatable 3D books to UI elements that refract through glass shaders:\n\n\nThe 3D book: A WebGL-rendered 3D book that uses HTML layout for its pages. Users can swap fonts with CSS. Because it's DOM-based, built-in translation works instantly, and AI agents can extract the text with less complexity.\nInteractive 3D UIs: A WebGPU jelly slider that refracts light based on an underlying 3D model, while still responding to standard HTML <input type=\"range\"> step attributes.\nAnimated textures: A dynamic 3D billboard rendering an animated SVG pencil using the DOM directly into a WebGL texture without needing a custom animation loop.\nRefractive overlays: An interactive typography layer distorted by a moving 3D cursor, yet fully selectable and searchable using find-in-page.\n\n\nCheck out the collection of demos created by the community. If you'd like your HTML-in-Canvas demo to be featured in this collection, create a pull request to add it.\n\nLimitations\n\nWhile powerful, the API has a few conscious limitations:\n\n\nCross-origin content: For security and privacy reasons, the API does not work with cross-origin iframe content.\nMain thread scrolling: HTML-in-canvas is drawn with JavaScript, which means that scrolling and animations cannot update independently of JavaScript, like they can outside canvas. Developers should carefully consider the performance characteristics of putting scrolling content inside canvas versus having the entire canvas scroll.\n\n\nFeedback\n\nIf you are experimenting with the HTML-in-Canvas API, we want to hear from you! You can sign up for the origin trial to enable the feature on your site while it's in the experimental phase to help us shape the API design. You can also file an issue to provide any feedback.\n\nResources\n\n\nHTML-in-Canvas support in Three.js\nHTML-in-Canvas in Three.js demo\nHTML-in-Canvas support in PlayCanvas: Developer documentation\nHTML-in-Canvas in PlayCanvas demo\nHTML-in-Canvas: Explainer\nModern Web Guidance for AI coding tools for HTML-in-Canvas\nChrome.dev demos for HTML-in-Canvas\nAwesome HTML-in-Canvas demo collection by the community\n\n\n  \n\n  \n\n\n  \n    \n    \n      \n    \n    \n  \n       \n    \n    \n  \n\n  Last updated 2026-05-19 UTC."
                ],
                "output": "readability",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:32.993851+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-10-23T10:09:32.985886+00:00",
                "index_texts": null,
                "output": "Introducing the HTML-in-Canvas API origin trial \u00a0|\u00a0 Blog \u00a0|\u00a0 Chrome for Developers",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:32.941574+00:00",
                "status": "succeeded"
            }
        ],
        "wget": [
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--reject=js,mjs,woff,woff2,ttf,otf,eot,mp4,m4v,mov,webm,ogv,mkv,avi,flv",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "--timestamping",
                    "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-10-23T10:09:32.932162+00:00",
                "index_texts": null,
                "output": "developer.chrome.com/blog/html-in-canvas-origin-trial.html",
                "pwd": "/data/archive/1792750005.575665",
                "schema": "ArchiveResult",
                "start_ts": "2026-10-23T10:09:27.564548+00:00",
                "status": "succeeded"
            }
        ]
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": null,
        "dom": null,
        "favicon": "favicon.ico",
        "git": null,
        "media": null,
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Introducing the HTML-in-Canvas API origin trial \u00a0|\u00a0 Blog \u00a0|\u00a0 Chrome for Developers",
        "warc": null,
        "wget": "developer.chrome.com/blog/html-in-canvas-origin-trial.html"
    },
    "link_dir": "/data/archive/1792750005.575665",
    "newest_archive_date": "2026-10-23T10:09:36.074794+00:00",
    "num_failures": 0,
    "num_outputs": 6,
    "oldest_archive_date": "2026-10-23T10:09:26.128192+00:00",
    "path": "/blog/html-in-canvas-origin-trial",
    "schema": "Link",
    "scheme": "https",
    "snapshot_id": "bf9fe9af-0aa3-484d-b53f-197d4307cd6a",
    "sources": [
        "/data/sources/1792750005-import.txt"
    ],
    "tags": null,
    "tags_str": "",
    "timestamp": "1792750005.575665",
    "title": "Introducing the HTML-in-Canvas API origin trial \u00a0|\u00a0 Blog \u00a0|\u00a0 Chrome for Developers",
    "updated": "2026-10-23T10:09:26.126071+00:00",
    "updated_date": "2026-10-23 10:09",
    "url": "https://developer.chrome.com/blog/html-in-canvas-origin-trial"
}