loading
Note:
Closed means closed to you too A closed root is unreachable from the page that created it as much as from anyone else - the element simply reports no root at all, and there is no way back in. That is what closing it means, and why open is the default here.

Support check

IsSupported

attachShadow is in every current engine. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.ShadowDom shadowDom

var supported = await shadowDom.IsSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Style isolation that actually holds

AttachShadow / AddStyle / SetHtml

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.

C#
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 in
Live sample
Ordinary element
the page's rule applies
Shadow host
attach output
Results will appear here when you interact with the samples.

Building the contents instead of writing HTML

Dom.Create / ShadowRootHandle.Append

SetHtml 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.

Razor
@inject Bit.Butil.Dom dom

@code {
    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);
    }
}
Live sample
Shadow host
build output
Results will appear here when you interact with the samples.

Reading into someone else's component

GetShadowRoot / Query / QueryAll

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.

C#
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]");
Live sample
In the document
a card the document can see
Behind a shadow root
query output
Results will appear here when you interact with the samples.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes attachShadow. Returns default (false) during prerender/SSR instead of throwing.
AttachShadow
ValueTask<ShadowRootHandle?> AttachShadow(ElementReference host, bool open = true, bool delegatesFocus = false)
Attaches a root. Null when the element already has one, or is one of the many that cannot have one at all.
AttachShadow (handle)
ValueTask<ShadowRootHandle?> AttachShadow(DomHandle host, bool open = true, bool delegatesFocus = false)
The same, for an element found or created through Dom.
GetShadowRoot
ValueTask<ShadowRootHandle?> GetShadowRoot(ElementReference host), GetShadowRoot(DomHandle host)
The root already on an element. Null when there is none - or when it is closed, which is indistinguishable from there being none.
ShadowRootHandle.Query / QueryAll
ValueTask<DomHandle?> Query(string selector), ValueTask<DomHandle[]> QueryAll(string selector)
Searching inside the root, which nothing outside can do.
ShadowRootHandle.GetHost
ValueTask<DomHandle?> GetHost()
The element the root is attached to - the way back out.
ShadowRootHandle.GetMode
ValueTask<string> GetMode()
"open" or "closed".
ShadowRootHandle.GetHtml / SetHtml
ValueTask<string> GetHtml(), ValueTask<bool> SetHtml(string html)
The root's contents as HTML. SetHtml parses, and is an injection point - isolation is of styles, not script.
ShadowRootHandle.Append
ValueTask<bool> Append(DomHandle child)
Puts an element inside the root. The safe way to add content you did not write.
ShadowRootHandle.AddStyle
ValueTask<bool> AddStyle(string css)
A stylesheet scoped to the root. Selectors are written against its own contents, with :host for the element it is attached to.
An unhandled error has occurred. Reload 🗙