ResizeObserver
React to element size changes the moment they happen: ObserveResize is an extension method on any ElementReference that reports content, border and device-pixel box sizes to a C# callback - the right tool for container queries, canvas sizing and responsive components, without window-resize polling.
Capture the element with the ref directive and call ObserveResize with the IJSRuntime and a handler. The browser reports an initial entry immediately, then again on every size change. The box below has a CSS resize handle in its bottom-right corner - start observing, then drag it.
IJSRuntime js
<div @ref="box" style="resize: both; overflow: auto;">Drag my corner</div>
{
private ElementReference box;
private ButilSubscription? subscription;
private async Task Start()
{
subscription = await box.ObserveResize(js, entries =>
{
foreach (var entry in entries)
{
Console.WriteLine($"{entry.InlineSize} x {entry.BlockSize}");
}
});
}
}The box parameter selects which box dimensions trigger the observer: ContentBox (the default) excludes padding and borders, BorderBox includes them, and DevicePixelContentBox measures in physical device pixels - the choice that matters when sizing a canvas backing store. Pick one below and re-observe.
IJSRuntime js
<div @ref="box" style="resize:both; overflow:auto">...</div>
{
private ElementReference box;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
await box.ObserveResize(js, entries =>
{
// fires when the border box changes size
}, ResizeObserverBox.BorderBox);
}
}Each entry flattens the browser's ResizeObserverEntry into the fields layout work actually needs: the content rect, the logical inline and block sizes of the observed box, and the device-pixel sizes for pixel-perfect rendering.
IJSRuntime js
<div @ref="box" style="resize:both; overflow:auto">...</div>
{
private ElementReference box;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
await box.ObserveResize(js, entries =>
{
var entry = entries[^1];
Rect? rect = entry.ContentRect; // content box geometry
var inline = entry.InlineSize; // width in horizontal writing mode
var block = entry.BlockSize; // height in horizontal writing mode
var dpInline = entry.DevicePixelInlineSize;
var dpBlock = entry.DevicePixelBlockSize;
});
}
}The observer does not care who changed the size: user drags, CSS state changes, Blazor re-renders and style writes all produce entries. Here the button widens the box through the Element extension SetAttribute while the observer reports each step.
<div @ref="box">...</div>
<button @onclick="Resize">Resize it from code</button>
{
private ElementReference box;
// Any style write triggers the observer just like a user drag - the observer watches the box,
// not the input that changed it.
private async Task Resize() =>
await box.SetAttribute("style", "width: 20rem; height: 6rem; resize: both; overflow: auto;");
}A ResizeObserver delivers an entry every frame for as long as a drag or a reflow lasts, and every entry is an interop round trip - under Blazor Server, a SignalR message and a network hop per frame. minInterval caps how often the handler is called. The gate is leading-edge with a trailing send, so the size the element settles at always arrives; it is just delivered a little later. Start a gated observer below and drag the box: the console fills at a fraction of the rate, and the last line still reads the final size.
IAsyncDisposable
IJSRuntime js
<div @ref="box" style="resize:both; overflow:auto">...</div>
{
private ElementReference box;
private ButilSubscription? subscription;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender is false) return;
subscription = await box.ObserveResize(js, entries =>
{
// called at most once every 100 ms, and always once more
// after the drag stops - with the settled size
},
minInterval: TimeSpan.FromMilliseconds(100));
}
public async ValueTask DisposeAsync()
{
if (subscription is not null) await subscription.DisposeAsync();
}
}ObserveResize returns a ButilSubscription; disposing it unobserves the element and releases the interop reference. Dispose every observer you start - at the latest in your component's DisposeAsync, as this page does.
IAsyncDisposable
private ButilSubscription? subscription;
public async ValueTask DisposeAsync()
{
if (subscription is not null)
{
await subscription.DisposeAsync();
}
}API reference
Task<ButilSubscription> ObserveResize(IJSRuntime js, Action<ResizeObserverEntry[]> handler, ResizeObserverBox box = ResizeObserverBox.ContentBox, TimeSpan? minInterval = null)enum ResizeObserverBox { ContentBox, BorderBox, DevicePixelContentBox }Rect? ContentRect { get; set; }double InlineSize { get; set; }double BlockSize { get; set; }double DevicePixelInlineSize { get; set; }double DevicePixelBlockSize { get; set; }Guid Id { get; }ValueTask DisposeAsync()