VisualViewport
Inspect the visual viewport - the portion of the page the user actually sees - including its offsets, size and pinch-zoom scale, and react to viewport resize and scroll events. Especially useful on mobile, where the on-screen keyboard and pinch-zoom shrink the visible area.
@inject Bit.Butil.VisualViewport visualViewportMDN reference
The visual viewport's dimensions in CSS pixels and the current pinch-zoom scaling factor (1 when not zoomed).
var width = await visualViewport.GetWidth();
var height = await visualViewport.GetHeight();
var scale = await visualViewport.GetScale();How far the visual viewport's edges sit from the layout viewport's edges, in CSS pixels. Both are 0 until the user pinch-zooms and pans.
var offsetLeft = await visualViewport.GetOffsetLeft();
var offsetTop = await visualViewport.GetOffsetTop();The visual viewport's coordinates relative to the document origin, in CSS pixels - these grow as the page is scrolled.
var pageLeft = await visualViewport.GetPageLeft();
var pageTop = await visualViewport.GetPageTop();Fires when the visual viewport is resized - window resize, pinch-zoom, or the on-screen keyboard appearing. Try resizing this window after subscribing.
var subscription = await visualViewport.SubscribeResize(() =>
{
// the visual viewport was resized
});
// later, when no longer needed:
await subscription.DisposeAsync();Fires when the visual viewport is scrolled. Note that this is the visual viewport, not the document - on desktop it mostly fires while pinch-zoomed. It fires once a frame for as long as a pinch-zoom pan lasts and carries no payload, so one event is a round trip saying 'look again' plus one for every property the handler then reads: pass minInterval to cap that, or use SubscribeScrollEnd below when only the resting position matters.
var subscription = await visualViewport.SubscribeScroll(() =>
{
// the visual viewport was scrolled
});
// or, capped at one call every 100 ms - the last one always
// arriving with the settled viewport:
var gated = await visualViewport.SubscribeScroll(
() => { /* ... */ },
minInterval: TimeSpan.FromMilliseconds(100));
// later, when no longer needed:
await subscription.DisposeAsync();Manual listener management
AddResize / AddScroll / RemoveResize / RemoveScroll / RemoveAllEventHandlersIf you prefer explicit ids over disposable subscriptions, the Add methods return a Guid that can later be passed to the matching Remove overload, and RemoveAllEventHandlers detaches everything registered on this instance.
// register and keep the ids
var resizeId = await visualViewport.AddResize(OnViewportResized);
var scrollId = await visualViewport.AddScroll(OnViewportScrolled);
// remove single listeners by id
await visualViewport.RemoveResize(resizeId);
await visualViewport.RemoveScroll(scrollId);
// or drop every registered listener at once
await visualViewport.RemoveAllEventHandlers();API reference
Task<double> GetOffsetLeft()Task<double> GetOffsetTop()Task<double> GetPageLeft()Task<double> GetPageTop()Task<double> GetWidth()Task<double> GetHeight()Task<double> GetScale()ValueTask<Guid> AddResize(Action handler, TimeSpan? minInterval = null)ValueTask<ButilSubscription> SubscribeResize(Action handler, TimeSpan? minInterval = null)ValueTask<Guid[]> RemoveResize(Action handler)ValueTask RemoveResize(Guid id)ValueTask<Guid> AddScroll(Action handler, TimeSpan? minInterval = null)ValueTask<ButilSubscription> SubscribeScroll(Action handler, TimeSpan? minInterval = null)ValueTask<Guid[]> RemoveScroll(Action handler)ValueTask RemoveScroll(Guid id)ValueTask RemoveAllEventHandlers()void InvokeVisualViewport(Guid id)ValueTask DisposeAsync()ValueTask<ButilSubscription> SubscribeScrollEnd(Action handler)