loading

Observe size changes

ObserveResize

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.

Razor
@inject IJSRuntime js

<div @ref="box" style="resize: both; overflow: auto;">Drag my corner</div>

@code {
    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}");
            }
        });
    }
}
Live sample
Drag my bottom-right corner to resize me.
observe output
Results will appear here when you interact with the samples.

Box models

ResizeObserverBox

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.

Razor
@inject IJSRuntime js

<div @ref="box" style="resize:both; overflow:auto">...</div>

@code {
    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);
    }
}
Live sample
Observed box
Drag my bottom-right corner to resize me.
box models output
Results will appear here when you interact with the samples.

Reading entries

ResizeObserverEntry

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.

Razor
@inject IJSRuntime js

<div @ref="box" style="resize:both; overflow:auto">...</div>

@code {
    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;
        });
    }
}
Live sample
Drag my bottom-right corner to resize me.
entries output
Results will appear here when you interact with the samples.

Programmatic changes fire too

ObserveResize + SetAttribute

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.

Razor
<div @ref="box">...</div>
<button @onclick="Resize">Resize it from code</button>

@code {
    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;");
}
Live sample
This box is widened from C#, not by dragging.
programmatic changes output
Results will appear here when you interact with the samples.

Rate limiting

minInterval

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.

Razor
@implements IAsyncDisposable
@inject IJSRuntime js

<div @ref="box" style="resize:both; overflow:auto">...</div>

@code {
    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();
    }
}
Live sample
Minimum interval (ms)
Drag my bottom-right corner to resize me.
rate limiting output
Results will appear here when you interact with the samples.

Stopping

ButilSubscription

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.

Razor
@implements IAsyncDisposable

private ButilSubscription? subscription;

public async ValueTask DisposeAsync()
{
    if (subscription is not null)
    {
        await subscription.DisposeAsync();
    }
}
Live sample
Drag my bottom-right corner to resize me.
stopping output
Results will appear here when you interact with the samples.
Note:
An initial entry arrives immediately Like the native API, observation starts with one report of the element's current size before any actual resize happens. That makes it safe to drive layout from the callback alone - you never need a separate initial measurement.

API reference

Member
Signature
Description
ObserveResize
Task<ButilSubscription> ObserveResize(IJSRuntime js, Action<ResizeObserverEntry[]> handler, ResizeObserverBox box = ResizeObserverBox.ContentBox, TimeSpan? minInterval = null)
Extension method on ElementReference. Starts observing size changes and returns a disposable subscription. minInterval rate-limits the handler in JavaScript, before the round trip.
ResizeObserverBox
enum ResizeObserverBox { ContentBox, BorderBox, DevicePixelContentBox }
Selects which box dimensions trigger the observer.
ResizeObserverEntry.ContentRect
Rect? ContentRect { get; set; }
The content box geometry of the observed element.
ResizeObserverEntry.InlineSize
double InlineSize { get; set; }
Logical inline size of the observed box (width in horizontal writing modes).
ResizeObserverEntry.BlockSize
double BlockSize { get; set; }
Logical block size of the observed box (height in horizontal writing modes).
ResizeObserverEntry.DevicePixelInlineSize
double DevicePixelInlineSize { get; set; }
Inline size in physical device pixels.
ResizeObserverEntry.DevicePixelBlockSize
double DevicePixelBlockSize { get; set; }
Block size in physical device pixels.
ButilSubscription.Id
Guid Id { get; }
The internal listener id of the subscription.
ButilSubscription.DisposeAsync
ValueTask DisposeAsync()
Stops observing and releases the interop reference. Idempotent.
An unhandled error has occurred. Reload 🗙