loading
Warning:
Not for your own components Every other DOM API in Butil is anchored to an ElementReference, which means an element your own markup rendered. That boundary exists because reaching into your own components by selector is how Blazor's diffing and your code end up disagreeing about what is on the page - Blazor will re-render over whatever you changed, at a moment you do not control. This is the way out for what the boundary does not cover: a third-party widget, something a script put there, an element you are building to hand to a library.

Support check

IsSupported

True when there is a document to query, which is any real browser. During prerender/SSR it returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.Dom dom

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

Finding things

Query / QueryAll / ById / Body / Head / DocumentElement

An invalid selector answers null rather than throwing: a selector is usually built from something a user or a configuration file supplied, so a bad one is an input error rather than an exceptional condition. QueryAll is a snapshot, not a live list - elements added afterwards are not in it.

C#
var widget = await dom.Query(".third-party-widget");
var all = await dom.QueryAll("[data-role]");
var byId = await dom.ById("some-id");
var head = await dom.Head();      // where a stylesheet or a script goes
Live sample

A little markup to search: bold, italic, and a span with an id.

  • one
  • two
  • three
query output
Results will appear here when you interact with the samples.

Building and placing

Create / Append / Prepend / InsertBefore / AppendTo / Remove

A created element is not on the page until something appends it. Moving an element that is already somewhere moves it - a node is in one place at a time, so there is no need to remove it first. Appending into one of your own rendered elements is possible but risky: Blazor owns that element's children and its next diff may remove what you put there, so use a container your markup leaves empty.

C#
var box = await dom.Create("div");
await box!.SetAttribute("class", "note");
await box.SetText("built from C#");

var body = await dom.Body();
await body!.Append(box);

// SVG needs its namespace, or it renders as nothing:
var circle = await dom.Create("circle", "http://www.w3.org/2000/svg");
Live sample
build output
Results will appear here when you interact with the samples.

Walking the tree

GetParent / GetChildren / GetFirstChild / GetNextSibling / Closest / Matches

Element-wise, not node-wise: the sibling and child accessors skip text and comment nodes, which is why raw node traversal so rarely does what you meant - the whitespace between two elements in your markup is a node too.

C#
var item = await dom.Query("ul[data-role] li");
var list = await item!.GetParent();
var second = await item.GetNextSibling();
var section = await item.Closest("[data-role]");
var isListItem = await item.Matches("li");
Live sample
traversal output
Results will appear here when you interact with the samples.

The bridge back

AsElementReference

This is what keeps the surface small: a found or created element becomes an ElementReference, and every element extension in Butil - classes, ARIA, styles, scrolling, layout, events - works on it. Two conditions: the element has to be in the document, and the lookup does not pierce shadow roots. It leans on the convention Blazor's own reference lookup uses, which is covered by a test, so a future change shows up here rather than in your app.

C#
var found = await dom.Query("#some-widget");
var reference = await found!.AsElementReference();

// and now the whole element surface applies:
await reference!.Value.AddClass("highlighted");
await reference.Value.SetStyleProperty("outline", "2px solid currentColor");
var rect = await reference.Value.GetBoundingClientRect();
Live sample
bridge output
Results will appear here when you interact with the samples.

Text and HTML

GetText / SetText / GetHtml / SetHtml

SetText is safe with any input - markup in it becomes visible characters rather than elements. SetHtml is an injection point: anything from a user, a URL or a server response can carry script. Not a script tag, which will not run, but an onerror on an img, which will. Use SetText for anything you did not write yourself.

Razor
@code {
    private DomHandle? handle;   // from dom.QuerySelector / CreateElement

    private async Task Write()
    {
        await handle!.SetText("<b>not bold</b>");   // shows the characters
        await handle.SetHtml("<b>bold</b>");        // parses - only for markup you wrote
    }
}
Live sample
content output
Results will appear here when you interact with the samples.
Note:
A handle is not an element Disposing a handle forgets the element; Remove takes it off the page. A handle to an element that has been removed keeps working - it is simply no longer connected. As a safety net the Dom service releases every handle when its scope is torn down, which likewise leaves the page alone.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when there is a document to query. Returns default (false) during prerender/SSR instead of throwing.
Query / QueryAll
ValueTask<DomHandle?> Query(string selector), ValueTask<DomHandle[]> QueryAll(string selector)
The first match, or all of them as a snapshot. An invalid selector answers null / empty rather than throwing.
ById
ValueTask<DomHandle?> ById(string elementId)
The element with this id, or null.
Body / Head / DocumentElement
ValueTask<DomHandle?> Body(), Head(), DocumentElement()
The document's landmarks - head for a stylesheet or script, documentElement for a theme attribute.
Create
ValueTask<DomHandle?> Create(string tagName, string? namespaceUri = null)
Creates an element, not yet on the page. SVG and MathML need their namespace or they render as nothing.
DomHandle.AsElementReference
ValueTask<ElementReference?> AsElementReference()
Turns the element into an ElementReference so every element extension works on it. Needs the element to be in the document; does not pierce shadow roots.
DomHandle.Query / QueryAll / Closest / Matches
ValueTask<DomHandle?> Query(string), ValueTask<DomHandle[]> QueryAll(string), ValueTask<DomHandle?> Closest(string), ValueTask<bool> Matches(string)
Searching inside this element, upwards from it, and testing it.
DomHandle traversal
ValueTask<DomHandle?> GetParent(), GetFirstChild(), GetLastChild(), GetNextSibling(), GetPreviousSibling(); ValueTask<DomHandle[]> GetChildren()
Element-wise: text and comment nodes are skipped.
DomHandle placement
ValueTask<bool> Append(DomHandle), Prepend(DomHandle), InsertBefore(DomHandle), AppendTo(ElementReference), Remove()
Moving an element that is already placed moves it. AppendTo puts it inside one of your own rendered elements - which Blazor may later diff away.
DomHandle content
ValueTask<string> GetText(), GetHtml(); ValueTask<bool> SetText(string), SetHtml(string)
SetText is safe with any input. SetHtml parses, and is an injection point.
DomHandle attributes
ValueTask<string?> GetAttribute(string); ValueTask<bool> SetAttribute(string, string), RemoveAttribute(string)
Attribute access. SetAttribute is false for a name that is not a valid attribute name.
DomHandle.IsConnected
ValueTask<bool> IsConnected()
Whether the element is still in the document - false for one created and never appended, and for one since removed.
DomHandle.DisposeAsync
ValueTask DisposeAsync()
Releases the handle. The element is untouched - this forgets it, it does not remove it.
An unhandled error has occurred. Reload 🗙