Element
Every Element and HTMLElement API as C# extension methods on Blazor's own ElementReference - no service to inject, no ids to invent. Capture a reference and call attributes, scrolling, layout metrics, pointer capture, fullscreen and DOM events straight from C#.
Element class to inject. Capture any rendered element
with @ref and the extension methods light up on the ElementReference itself.
Only the event subscription and observer extensions additionally need an IJSRuntime argument.
Attributes
GetAttribute / SetAttribute / HasAttribute / ToggleAttribute / RemoveAttribute / GetAttributeNamesRead, write, toggle and enumerate the attributes of the target element. ToggleAttribute is handy for boolean attributes such as hidden or disabled; pass force to pin the state instead of flipping it.
<div @ref="box" data-demo="butil">...</div>
{
private ElementReference box;
private async Task InspectAttributes()
{
var has = await box.HasAttribute("data-demo");
var value = await box.GetAttribute("data-demo");
var names = await box.GetAttributeNames();
await box.SetAttribute("data-demo", "updated");
await box.ToggleAttribute("hidden", force: null);
await box.RemoveAttribute("data-demo");
}
}Scroll inside an element to absolute coordinates, scroll by a relative amount, or bring the element itself into the viewport. All three accept an options object with smooth, instant or auto behavior; ScrollIntoView additionally takes block and inline logical positions.
<div @ref="box" style="overflow:auto; max-height:12rem">...</div>
{
private ElementReference box;
private async Task Scroll()
{
await box.Scroll(new ScrollOptions { Top = 120, Behavior = ScrollBehavior.Smooth });
await box.ScrollBy(0, 48);
await box.ScrollIntoView(new ScrollIntoViewOptions
{
Behavior = ScrollBehavior.Smooth,
Block = ScrollLogicalPosition.Center,
Inline = ScrollLogicalPosition.Nearest,
});
}
}Read the element's box-model geometry: the viewport-relative bounding rect, client sizes (padding box), offset sizes (border box and position within the offset parent), scroll sizes and offsets, and the tag name.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Measure()
{
var rect = await box.GetBoundingClientRect(); // Rect { X, Y, Width, Height }
var clientWidth = await box.GetClientWidth();
var offsetHeight = await box.GetOffsetHeight();
var scrollHeight = await box.GetScrollHeight();
var scrollTop = await box.GetScrollTop();
var tagName = await box.GetTagName();
}
}The box being measured. It scrolls, so the scroll metrics have something to report.
Its padding box, border box and scroll box are all different sizes.
Scroll it and read the offsets back.
One more line, to make sure it overflows.
Focus and pointer capture
Blur / SetPointerCapture / HasPointerCapture / ReleasePointerCapture / RequestPointerLockBlur removes keyboard focus from an element (for setting focus, Blazor's built-in ElementReference.FocusAsync already covers it). Pointer capture routes all subsequent pointer events to one element - the building block for custom drag interactions. RequestPointerLock hides the cursor and streams raw movement deltas, as games do.
<input @ref="input" />
<div @ref="box" @onpointerdown="Capture">...</div>
{
private ElementReference box;
private ElementReference input;
private async Task Capture(PointerEventArgs args)
{
await input.Blur();
// The id comes from the pointer event that started the gesture - capture is per pointer.
await box.SetPointerCapture(args.PointerId);
var captured = await box.HasPointerCapture(args.PointerId);
await box.ReleasePointerCapture(args.PointerId);
// Needs a user gesture, and the browser shows its own notice on the way in.
await box.RequestPointerLock();
}
}Pointer id: click this section's box first
Ask the browser to present the element fullscreen. The options object controls whether the browser keeps its navigation UI visible; press Escape to leave fullscreen again. Browsers only honor the request from a user gesture such as a click.
<div @ref="box">...</div>
<button @onclick="GoFullScreen">Full screen</button>
{
private ElementReference box;
// Must run inside a user gesture: a request that did not come from one is refused.
private async Task GoFullScreen() =>
await box.RequestFullScreen(new FullScreenOptions
{
NavigationUI = FullScreenNavigationUI.Show,
});
}Get and set the element's id, class list, rendered text, raw markup, access key and tab order position. The InnerHtml and OuterHtml setters write markup verbatim - never feed them untrusted input.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Rewrite()
{
await box.SetId("hero");
await box.SetClassName("card highlighted");
await box.SetInnerText("Hello from C#");
await box.SetTabIndex(2);
await box.SetAccessKey("k");
var html = await box.GetInnerHtml();
await box.SetInnerHtml("<strong>Trusted markup only!</strong>");
var outer = await box.GetOuterHtml();
}
}Editing and interaction modes
ContentEditable / IsContentEditable / Dir / EnterKeyHint / InputMode / Hidden / InertToggle in-place editing, text direction, virtual-keyboard hints, visibility and inertness on the target element. After setting ContentEditable to True, click into this section's box and type.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task SetModes()
{
await box.SetContentEditable(ContentEditable.True);
var editable = await box.IsContentEditable();
await box.SetDir(ElementDir.Rtl);
await box.SetEnterKeyHint(EnterKeyHint.Send);
await box.SetInputMode(InputMode.Numeric);
await box.SetHidden(Hidden.UntilFound);
await box.SetInert(true);
}
}Subscribe to any DOM event on the element with a strongly typed args class per event family - mouse, keyboard, pointer, touch, wheel, focus, input, drag, clipboard and composition. The generic argument must match the event: ButilEvents.Click pairs with ButilMouseEventArgs, ButilEvents.KeyDown with ButilKeyboardEventArgs, and so on. A second overload takes ButilEventListenerOptions for capture, passive and once semantics.
@inject IJSRuntime js
private ButilSubscription? clickSub;
clickSub = await box.SubscribeEvent<ButilMouseEventArgs>(
js, ButilEvents.Click, args => Console.WriteLine(args.OffsetX));
// capture / passive / once via options:
await input.SubscribeEvent<ButilKeyboardEventArgs>(
js, ButilEvents.KeyDown, args => Console.WriteLine(args.Key),
new ButilEventListenerOptions { Once = true });
// element subscriptions must be disposed:
await clickSub.DisposeAsync();Matches tests the element against any CSS selector. Remove detaches the element from the DOM - reload the page to bring the demo target back.
<div @ref="box" data-demo="butil">...</div>
{
private ElementReference box;
private async Task Check()
{
var isMatch = await box.Matches("[data-demo]");
// Detaches the element from the DOM. Blazor still believes it rendered it, so anything
// touching the reference afterwards is talking about a node nothing can see.
await box.Remove();
}
}Click sends a synthetic click - the element's own handlers run and default behavior happens, though it does not count as a user gesture. Focus is the overload Blazor's FocusAsync does not give you: preventScroll, for moving focus without yanking the page. CheckVisibility answers what an IntersectionObserver cannot - whether the element is transparent, collapsed, or inside a skipped content-visibility subtree. Closest reports whether the element sits inside anything matching a selector.
<form>
<button @ref="button" type="button">Target</button>
<input @ref="input" />
<div @ref="box">...</div>
</form>
{
private ElementReference box;
private ElementReference input;
private ElementReference button;
private async Task Activate()
{
await button.Click();
await input.Focus(new FocusOptions { PreventScroll = true });
var visible = await box.CheckVisibility(new CheckVisibilityOptions
{
OpacityProperty = true,
VisibilityProperty = true,
ContentVisibilityAuto = true,
});
var insideForm = await box.Closest("form");
}
}Classes, data attributes and inline style
AddClass / RemoveClass / ToggleClass / ReplaceClass / ContainsClass / GetClassList / GetData / SetData / RemoveData / GetDataNames / GetStyleText / SetStyleText / GetStyleProperty / SetStyleProperty / RemoveStylePropertyclassList, dataset and style as individual operations rather than as one string you have to parse and rebuild. SetStyleProperty takes CSS property names, custom properties included, so a theme variable can be written from C#. Blazor owns the DOM it rendered and a re-render can undo all of this - use it on elements Blazor renders once, or on ones outside the component.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Style()
{
await box.AddClass("card", "highlighted");
var toggled = await box.ToggleClass("selected");
var swapped = await box.ReplaceClass("highlighted", "muted");
var classes = await box.GetClassList();
await box.SetData("userId", "42"); // writes data-user-id
var id = await box.GetData("userId");
var keys = await box.GetDataNames();
await box.SetStyleProperty("--accent", "#7c3aed");
await box.SetStyleProperty("outline", "2px solid var(--accent)", important: true);
var css = await box.GetStyleText();
}
}Content insertion and serialization
Append / Prepend / Before / After / ReplaceChildren / ReplaceWith / InsertAdjacentText / InsertAdjacentHtml / GetHtml / SetHtml / SetHtmlUnsafeThe DOM insertion methods take elements as well as strings, but only strings can cross the interop boundary - an ElementReference is minted by Blazor's renderer and cannot be handed back from JavaScript. So each string becomes a text node, and InsertAdjacentHtml is how markup gets in. SetHtml runs the browser's sanitizer over what you give it, which is the one to reach for when the markup came from a user; it throws rather than silently falling back where the browser has no sanitizer.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Insert(string untrustedMarkup)
{
await box.Append("appended text");
await box.Prepend("prepended text");
await box.ReplaceChildren("everything else is gone");
await box.InsertAdjacentText(InsertPosition.BeforeEnd, "<not markup>");
await box.InsertAdjacentHtml(InsertPosition.AfterBegin, "<em>trusted markup</em>");
await box.SetHtml(untrustedMarkup); // sanitized; throws where unsupported
var html = await box.GetHtml(new GetHtmlOptions { SerializableShadowRoots = true });
}
}ARIA and roles
GetAriaLabel / SetAriaLabel / SetAriaExpanded / SetAriaHidden / GetRole / SetRole / AriaNotifyEvery aria-* attribute and role, reflected as a property. Prefer writing these as attributes in your markup - it costs no interop and a re-render cannot undo it; reach for these when the value depends on something outside the render tree, or when reading what a component library put on an element. Every value is a string, numeric and boolean ones included, because that is how ARIA itself is defined. AriaNotify announces a message without changing the page; it is Chromium-only and a no-op elsewhere, so keep the visible UI telling the same story.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Describe()
{
await box.SetRole("button");
await box.SetAriaLabel("Close the dialog");
await box.SetAriaExpanded("true");
await box.SetAriaHidden("false");
var label = await box.GetAriaLabel();
await box.AriaNotify("Upload finished",
new AriaNotifyOptions { Priority = AriaNotifyPriority.High });
}
}Turn any element into a popover and drive it from C#. An Auto popover is light-dismissed - Escape or a click outside closes it, and opening one closes the others; a Manual one closes only when the code says so. Show and Hide are no-ops where the browser has no popover support, so a page can call them without feature-detecting first.
<div @ref="panel">...</div>
{
private ElementReference panel;
private async Task Popover()
{
// The attribute is what makes it a popover; without it the three calls below throw.
await panel.SetPopover(ElementPopover.Auto);
await panel.ShowPopover();
var showing = await panel.TogglePopover();
await panel.HidePopover();
}
}Namespaced attributes, queries and scroll offsets
GetAttributeNS / SetAttributeNS / HasAttributeNS / RemoveAttributeNS / QuerySelectorMatches / QuerySelectorAllCount / GetClientRects / ScrollTo / SetScrollTop / SetScrollLeftThe namespaced attribute methods are what SVG's xlink:href and XML's xml:lang need, and what GetAttribute cannot reach. The query helpers answer the existence and count questions - the elements themselves cannot cross the boundary, so capture the ones you need with @ref. GetClientRects returns one rect per line box, which is more than one for an inline element that wraps. SetScrollTop and SetScrollLeft jump without animating, where ScrollTo can ease.
<svg><use @ref="icon" /></svg>
<table @ref="table"><tr><td>...</td></tr></table>
<em @ref="inlineText">a long run of text that wraps over several lines</em>
<div @ref="box" style="overflow:auto; max-height:12rem">...</div>
{
private const string Xlink = "http://www.w3.org/1999/xlink";
private ElementReference box;
private ElementReference icon;
private ElementReference table;
private ElementReference inlineText;
private async Task Read()
{
await icon.SetAttributeNS(Xlink, "xlink:href", "#star");
var href = await icon.GetAttributeNS(Xlink, "href");
var hasRows = await table.QuerySelectorMatches("tr");
var rowCount = await table.QuerySelectorAllCount("tr");
var rects = await inlineText.GetClientRects(); // one per line box
await box.SetScrollTop(0);
await box.ScrollTo(new ScrollOptions { Top = 240, Behavior = ScrollBehavior.Smooth });
}
}This inline span is long enough to wrap over several line boxes, which is what makes GetClientRects report more than one rectangle for it - one per line the text occupies.
- first
- second
- third
Identity, hints and tree facts
Title / Lang / Draggable / Spellcheck / Translate / Autofocus / Autocapitalize / Autocorrect / WritingSuggestions / VirtualKeyboardPolicy / Slot / Part / ElementTiming / LocalName / NamespaceUri / Prefix / ChildElementCount / CurrentCssZoom / AccessKeyLabel / OffsetParentTagName / AssignedSlotName / HasShadowRoot / OuterTextThe rest of what an element carries: its tooltip and language, the hints it gives a virtual keyboard and a spell checker, its shadow-DOM wiring, and the facts about where it sits in the tree. Most of these are attributes you can simply render from Blazor at no interop cost - these are for elements that are not yours to re-render, and for reading what something else put there.
<div @ref="box">...</div>
{
private ElementReference box;
private async Task Annotate()
{
await box.SetTitle("Shown on hover");
await box.SetLang("fa-IR");
await box.SetDraggable(true);
await box.SetSpellcheck(false);
await box.SetTranslate(false);
await box.SetAutocapitalize(Autocapitalize.Words);
await box.SetWritingSuggestions(false);
await box.SetSlot("content"); // which shadow slot it asks to land in
await box.SetPart("surface highlight"); // what ::part() outside the shadow tree can style
await box.SetElementTiming("hero-image"); // the name Element Timing reports it under
var localName = await box.GetLocalName(); // "div", where GetTagName gives "DIV"
var children = await box.GetChildElementCount();
var zoom = await box.GetCurrentCssZoom();
var offsetParent = await box.GetOffsetParentTagName();
}
}Audio and video
Play / Pause / GetMediaState / SetCurrentTime / SetVolume / SetMuted / SetPlaybackRateBlazor can render a media element and bind its events, but playback is imperative: there is no markup for 'play now' or 'seek to 30 seconds'. These extensions fill that gap on any ElementReference pointing at an <audio> or <video>. GetMediaState reads the whole transport state in one round trip rather than a property at a time - every read is an interop hop, and polling a dozen of them per frame to draw a scrubber is a dozen messages per frame.
<video @ref="_video" src="clip.webm" playsinline></video>
// starting playback can be refused - autoplay outside a gesture, or an undecodable source:
var started = await _video.Play();
await _video.SetVolume(0.4); // clamped to 0-1; ignored on iOS, where volume is hardware
await _video.SetMuted(true); // the mute toggle that works everywhere
await _video.SetPlaybackRate(1.5);
await _video.SetCurrentTime(30); // false when the media isn't seekable yet
var state = await _video.GetMediaState();
// state.Paused / CurrentTime / Duration / BufferedEnd / ReadyState / VideoWidth ...
await _video.Pause();Reparenting with insertBefore disconnects the node first, and everything living inside it goes with it: an iframe reloads, a playing video stops, a running animation restarts, focus is lost. moveBefore does the same move without the disconnection - which is what makes moving a node a reasonable thing to do at all. Pass null as the reference to append at the end.
<div @ref="column">
<div @ref="marker">a sibling to insert before</div>
</div>
<div @ref="card">
<video controls></video>
</div>
{
private ElementReference card;
private ElementReference column;
private ElementReference marker;
private async Task Move()
{
// move `card` into `column`, before `marker`
var moved = await column.MoveBefore(card, marker);
// or append it at the end
await column.MoveBefore(card);
}
}Blazor owns the DOM it rendered, so a re-render can undo the move - use this for elements Blazor does not re-render.
SubscribeEvent has no owning service to clean up after you: every subscription holds a
.NET object reference and a JS-side handler until its ButilSubscription is disposed.
Store the handle and dispose it in DisposeAsync, exactly as this page does.
API reference (extension methods on ElementReference)
ValueTask<string> GetAttribute(string name)ValueTask SetAttribute(string name, string value)ValueTask<string[]> GetAttributeNames()ValueTask<bool> HasAttribute(string name)ValueTask<bool> HasAttributes()ValueTask<bool> ToggleAttribute(string name, bool? force)ValueTask RemoveAttribute(string name)ValueTask<Rect> GetBoundingClientRect()ValueTask Scroll(ScrollOptions? options)ValueTask Scroll(double? x, double? y)ValueTask ScrollBy(ScrollOptions? options)ValueTask ScrollBy(double? x, double? y)ValueTask ScrollIntoView()ValueTask ScrollIntoView(bool alignToTop)ValueTask ScrollIntoView(ScrollIntoViewOptions options)ValueTask Blur()ValueTask SetPointerCapture(int pointerId)ValueTask<bool> HasPointerCapture(int pointerId)ValueTask ReleasePointerCapture(int pointerId)ValueTask RequestPointerLock()ValueTask RequestFullScreen(FullScreenOptions? options)ValueTask<bool> Matches(string selectors)ValueTask Remove()ValueTask<string> GetId() · ValueTask SetId(string id)ValueTask<string> GetClassName() · ValueTask SetClassName(string className)ValueTask<string> GetInnerHtml() · ValueTask SetInnerHtml(string innerHtml)ValueTask<string> GetOuterHtml() · ValueTask SetOuterHtml(string outerHtml)ValueTask<string> GetInnerText() · ValueTask SetInnerText(string value)ValueTask<string> GetAccessKey() · ValueTask SetAccessKey(string key)ValueTask<int> GetTabIndex() · ValueTask SetTabIndex(int value)ValueTask<float> GetClientHeight()ValueTask<float> GetClientWidth()ValueTask<float> GetClientTop()ValueTask<float> GetClientLeft()ValueTask<float> GetOffsetHeight()ValueTask<float> GetOffsetWidth()ValueTask<float> GetOffsetTop()ValueTask<float> GetOffsetLeft()ValueTask<float> GetScrollHeight()ValueTask<float> GetScrollWidth()ValueTask<float> GetScrollTop()ValueTask<float> GetScrollLeft()ValueTask<string> GetTagName()ValueTask<ContentEditable> GetContentEditable() · ValueTask SetContentEditable(ContentEditable value)ValueTask<bool> IsContentEditable()ValueTask<ElementDir> GetDir() · ValueTask SetDir(ElementDir value)ValueTask<EnterKeyHint> GetEnterKeyHint() · ValueTask SetEnterKeyHint(EnterKeyHint value)ValueTask<InputMode> GetInputMode() · ValueTask SetInputMode(InputMode value)ValueTask<Hidden> GetHidden() · ValueTask SetHidden(Hidden value)ValueTask<bool> GetInert() · ValueTask SetInert(bool value)Task<ButilSubscription> SubscribeEvent<T>(IJSRuntime js, string domEvent, Action<T> listener, bool useCapture = false, bool preventDefault = false, bool stopPropagation = false)Task<ButilSubscription> SubscribeEvent<T>(IJSRuntime js, string domEvent, Action<T> listener, ButilEventListenerOptions options, bool preventDefault = false, bool stopPropagation = false)TimeSpan? MinInterval { get; set; }ValueTask<bool> Play()ValueTask Pause()ValueTask Load()ValueTask<MediaElementState?> GetMediaState()ValueTask<bool> SetCurrentTime(double seconds)ValueTask SetVolume(double volume)ValueTask SetMuted(bool muted)ValueTask SetLoop(bool loop)ValueTask<bool> SetPlaybackRate(double rate)ValueTask SetMediaSource(string src)ValueTask<string> CanPlayType(string mimeType)ValueTask Click()ValueTask Focus(FocusOptions? options = null)ValueTask<bool> CheckVisibility(CheckVisibilityOptions? options = null)ValueTask<bool> Closest(string selectors)ValueTask ScrollTo(ScrollOptions? options) · ValueTask ScrollTo(double? x, double? y)ValueTask SetScrollTop(double value) · ValueTask SetScrollLeft(double value)ValueTask<float> GetScrollTopMax() · ValueTask<float> GetScrollLeftMax()ValueTask<string> GetAttributeNS(string namespaceUri, string localName) · ValueTask SetAttributeNS(string namespaceUri, string qualifiedName, string value)ValueTask<bool> HasAttributeNS(string namespaceUri, string localName) · ValueTask RemoveAttributeNS(string namespaceUri, string localName)ValueTask<Rect[]> GetClientRects()ValueTask Append(params string[] nodes)ValueTask ReplaceChildren(params string[] nodes) · ValueTask ReplaceWith(params string[] nodes)ValueTask InsertAdjacentHtml(InsertPosition position, string html)ValueTask<bool> IsMoveBeforeSupported()ValueTask<bool> MoveBefore(ElementReference node, ElementReference? reference = null)ValueTask InsertAdjacentText(InsertPosition position, string text)ValueTask<string> GetHtml(GetHtmlOptions? options = null)ValueTask SetHtml(string html)ValueTask SetHtmlUnsafe(string html)ValueTask AddClass(params string[] tokens) · ValueTask RemoveClass(params string[] tokens)ValueTask<bool> ToggleClass(string token, bool? force = null)ValueTask<bool> ReplaceClass(string oldToken, string newToken)ValueTask<bool> ContainsClass(string token) · ValueTask<string[]> GetClassList()ValueTask<string?> GetData(string key) · ValueTask SetData(string key, string value) · ValueTask RemoveData(string key)ValueTask<string[]> GetDataNames()ValueTask<string> GetStyleText() · ValueTask SetStyleText(string value)ValueTask<string> GetStyleProperty(string name) · ValueTask SetStyleProperty(string name, string value, bool important = false)ValueTask<string> RemoveStyleProperty(string name)ValueTask ShowPopover() · ValueTask HidePopover()ValueTask<bool> TogglePopover(bool? force = null)ValueTask<ElementPopover> GetPopover() · ValueTask SetPopover(ElementPopover value)ValueTask<bool> QuerySelectorMatches(string selectors)ValueTask<int> QuerySelectorAllCount(string selectors)ValueTask<string> GetAriaLabel() · ValueTask SetAriaLabel(string value)ValueTask<string> GetRole() · ValueTask SetRole(string value)ValueTask AriaNotify(string message, AriaNotifyOptions? options = null)ValueTask<string> GetTitle() · ValueTask SetTitle(string value)ValueTask<string> GetLang() · ValueTask SetLang(string value)ValueTask<bool> GetDraggable() · ValueTask SetDraggable(bool value)ValueTask<bool> GetSpellcheck() · ValueTask SetSpellcheck(bool value)ValueTask<bool> GetTranslate() · ValueTask SetTranslate(bool value)ValueTask<bool> GetAutofocus() · ValueTask SetAutofocus(bool value)ValueTask<Autocapitalize> GetAutocapitalize() · ValueTask SetAutocapitalize(Autocapitalize value)ValueTask<bool> GetAutocorrect() · ValueTask SetAutocorrect(bool value)ValueTask<bool> GetWritingSuggestions() · ValueTask SetWritingSuggestions(bool value)ValueTask<VirtualKeyboardPolicy> GetVirtualKeyboardPolicy() · ValueTask SetVirtualKeyboardPolicy(VirtualKeyboardPolicy value)ValueTask<string> GetSlot() · ValueTask SetSlot(string value)ValueTask<string> GetAssignedSlotName()ValueTask<string[]> GetPart() · ValueTask SetPart(string value)ValueTask<bool> HasShadowRoot()ValueTask<string> GetElementTiming() · ValueTask SetElementTiming(string value)ValueTask<string> GetLocalName()ValueTask<string> GetNamespaceUri() · ValueTask<string> GetPrefix()ValueTask<int> GetChildElementCount()ValueTask<double> GetCurrentCssZoom()ValueTask<string> GetAccessKeyLabel()ValueTask<string> GetOffsetParentTagName()ValueTask<string> GetOuterText() · ValueTask SetOuterText(string value)ValueTask<string> GetNonce() · ValueTask SetNonce(string value)