//components/surface_embed implements a mechanism to embed a content::WebContents (the inner/embedded page) inside another content::WebContents (the outer/embedder page) by using a custom blink::WebPlugin. It is utilized by WebUI Browser to embed tab contents directly.
Currently, surface-embed is only enabled for chrome://webui-browser. If you wish to allow another frame or WebUI domain to use surface-embed, you must:
SurfaceEmbedHost in chrome_content_browser_client_receiver_bindings.cc:#include "components/surface_embed/common/features.h" #include "components/surface_embed/browser/surface_embed_host.h" // ... if (base::FeatureList::IsEnabled(surface_embed::features::kSurfaceEmbed)) { associated_registry.AddInterface<surface_embed::mojom::SurfaceEmbedHost>( base::BindRepeating( [](content::RenderFrameHost* render_frame_host, mojo::PendingAssociatedReceiver< surface_embed::mojom::SurfaceEmbedHost> receiver) { auto* web_ui = render_frame_host->GetWebUI(); // Security check: only allow surface-embed in WebUIBrowserUI. // Add your WebUIController here. if (!web_ui || !web_ui->GetController()->GetAs<WebUIBrowserUI>()) { return; } surface_embed::SurfaceEmbedHost::Create(render_frame_host, std::move(receiver)); }, &render_frame_host)); }
if (url.SchemeIs(content::kChromeUIScheme) && url.host() == chrome::kChromeUIYourCustomHost) { if (surface_embed::MaybeCreatePlugin(render_frame, params, plugin)) { return true; } }
By default, WebUI blocks <object>s and <embed>s. You must explicitly override the CSP object-src rule to allow 'self' via WebUIDataSource.
source->OverrideContentSecurityPolicy( network::mojom::CSPDirectiveName::ObjectSrc, "object-src 'self';");
To embed a child content::WebContents, the frontend needs a unique identifier corresponding to that embedded WebContents. This is managed by SurfaceEmbedHandle.
On the C++ side, associate a handle with the embedded WebContents and retrieve its assigned unique ID token (base::UnguessableToken):
#include "components/surface_embed/browser/surface_embed_handle.h" // 1. Create or retrieve the handle for the target WebContents surface_embed::SurfaceEmbedHandle* embedded_handle = surface_embed::SurfaceEmbedHandle::CreateForWebContents(web_contents); // 2. Get the unique ID token and pass its serialized string to the frontend // (e.g. via Mojo message) std::string content_id = embedded_handle->id().ToString();
Surface Embed does not manage the lifecycle of WebContents. If the child WebContents is destroyed, the <embed> will become blank. If the <embed> is removed, the previously attached WebContents will be detached. Changing data-content-id on an existing <embed> will detach the previously attached WebContents (if any) and attach the new WebContents (setting data-content-id to the same value is a no-op).
To embed the child page on your WebUI frontend, simply instantiate the <embed> tag with your embedded WebContents's content ID.
<embed> ElementIn your template:
import {html} from '//resources/lit/v3_0/lit.rollup.js'; // Inside your custom WebUI component render() helper: return html` <embed class="content" type="application/x-chromium-surface-embed" data-content-id="${this.contentId}"> </embed> `;
type: Must match kInternalPluginMimeType, which is "application/x-chromium-surface-embed".data-content-id: Must contain the serialized string representation of the surface_embed::SurfaceEmbedHandle token corresponding to the nested WebContents (the content_id retrieved in Section 1.3).data-allow-pinch-zoom: Optional boolean attribute. When present, touchscreen and touchpad pinch gestures that start over the embedded content scale the embedded WebContents; when absent, they scale the embedding WebContents. Adding or removing the attribute affects future pinch sequences. A pinch sequence already in progress keeps the target selected when that sequence began. As with other HTML boolean attributes, the value is ignored, so data-allow-pinch-zoom="false" still enables child pinch zoom.