ShadowDom
attachShadow and the roots it produces: a subtree with its own styles that the page's CSS does not reach into and whose own CSS does not leak out - and the way into a third-party web component's real markup.
@inject Bit.Butil.ShadowDom shadowDomMDN reference
attachShadow is in every current engine. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.
@inject Bit.Butil.ShadowDom shadowDom
var supported = await shadowDom.IsSupported();The page below has a rule that turns every .card red. The left box is an ordinary element and obeys it. The right one gets a shadow root, and the same rule cannot reach inside - so its own AddStyle wins, and nothing it defines escapes back out. This is the only reliable style isolation the platform has. Note that Blazor renders into the host's light DOM, and light-DOM content of a shadow host is not displayed unless the root has a slot for it: attach to a container your markup leaves empty.
private ElementReference _host;
var root = await shadowDom.AttachShadow(_host);
await root!.AddStyle(".card { color: seagreen; border: 1px solid seagreen; }");
await root.SetHtml("<div class='card'>inside a shadow root</div>");
// the page's own `.card { color: red }` does not reach inSetHtml is an injection point exactly as it is on an ordinary element - being inside a shadow root isolates styles, not script. Build the contents from elements for anything you did not write yourself.
Bit.Butil.Dom dom
{
private DomHandle? root; // the shadow root from shadowDom.Attach
// Building nodes rather than writing a markup string: SetText cannot introduce an element, so
// there is no string for anything to be injected into.
private async Task Build(string userSuppliedText)
{
var badge = await dom.Create("span");
await badge!.SetAttribute("class", "card");
await badge.SetText(userSuppliedText); // safe with any input
await root!.Append(badge);
}
}This is the interoperability case: a web component's real markup lives inside its shadow root, where an ordinary querySelector cannot see it. Find the host with Dom, ask for its root, and query from there. Selectors from outside do not reach in at all - which is the isolation working, not a bug to route around. Note that AsElementReference does not resolve for an element inside a shadow root: Blazor's own reference lookup does not pierce the boundary either.
var host = await dom.Query("some-web-component");
var root = await shadowDom.GetShadowRoot(host!);
var button = await root!.Query("button.internal");
var all = await root.QueryAll("[part]");API reference
ValueTask<bool> IsSupported()ValueTask<ShadowRootHandle?> AttachShadow(ElementReference host, bool open = true, bool delegatesFocus = false)ValueTask<ShadowRootHandle?> AttachShadow(DomHandle host, bool open = true, bool delegatesFocus = false)ValueTask<ShadowRootHandle?> GetShadowRoot(ElementReference host), GetShadowRoot(DomHandle host)ValueTask<DomHandle?> Query(string selector), ValueTask<DomHandle[]> QueryAll(string selector)ValueTask<DomHandle?> GetHost()ValueTask<string> GetMode()ValueTask<string> GetHtml(), ValueTask<bool> SetHtml(string html)ValueTask<bool> Append(DomHandle child)ValueTask<bool> AddStyle(string css)