loading

Size & scale

GetWidth / GetHeight / GetScale

The visual viewport's dimensions in CSS pixels and the current pinch-zoom scaling factor (1 when not zoomed).

C#
var width = await visualViewport.GetWidth();
var height = await visualViewport.GetHeight();
var scale = await visualViewport.GetScale();
Live sample
size output
Results will appear here when you interact with the samples.

Offset from the layout viewport

GetOffsetLeft / GetOffsetTop

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.

C#
var offsetLeft = await visualViewport.GetOffsetLeft();
var offsetTop = await visualViewport.GetOffsetTop();
Live sample
offset output
Results will appear here when you interact with the samples.

Position on the page

GetPageLeft / GetPageTop

The visual viewport's coordinates relative to the document origin, in CSS pixels - these grow as the page is scrolled.

C#
var pageLeft = await visualViewport.GetPageLeft();
var pageTop = await visualViewport.GetPageTop();
Live sample
page position output
Results will appear here when you interact with the samples.

Resize event

SubscribeResize

Fires when the visual viewport is resized - window resize, pinch-zoom, or the on-screen keyboard appearing. Try resizing this window after subscribing.

C#
var subscription = await visualViewport.SubscribeResize(() =>
{
    // the visual viewport was resized
});

// later, when no longer needed:
await subscription.DisposeAsync();
Live sample
resize event output
Results will appear here when you interact with the samples.

Scroll event

SubscribeScroll

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.

C#
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();
Live sample
scroll event output
Results will appear here when you interact with the samples.

Manual listener management

AddResize / AddScroll / RemoveResize / RemoveScroll / RemoveAllEventHandlers

If 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.

C#
// 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();
Note:
Visual vs. layout viewport The layout viewport is what CSS lays the page out against; the visual viewport is the part currently on screen. They only diverge when the user pinch-zooms or an on-screen keyboard appears, which is why the offset and scale demos are best tried on a mobile device.

API reference

Member
Signature
Description
GetOffsetLeft
Task<double> GetOffsetLeft()
Returns the offset of the left edge of the visual viewport from the left edge of the layout viewport, in CSS pixels.
GetOffsetTop
Task<double> GetOffsetTop()
Returns the offset of the top edge of the visual viewport from the top edge of the layout viewport, in CSS pixels.
GetPageLeft
Task<double> GetPageLeft()
Returns the x coordinate of the left edge of the visual viewport relative to the initial containing block origin, in CSS pixels.
GetPageTop
Task<double> GetPageTop()
Returns the y coordinate of the top edge of the visual viewport relative to the initial containing block origin, in CSS pixels.
GetWidth
Task<double> GetWidth()
Returns the width of the visual viewport, in CSS pixels.
GetHeight
Task<double> GetHeight()
Returns the height of the visual viewport, in CSS pixels.
GetScale
Task<double> GetScale()
Returns the pinch-zoom scaling factor applied to the visual viewport.
AddResize
ValueTask<Guid> AddResize(Action handler, TimeSpan? minInterval = null)
Registers a handler for the resize event and returns its listener id. minInterval rate-limits the handler in JavaScript, before the round trip.
SubscribeResize
ValueTask<ButilSubscription> SubscribeResize(Action handler, TimeSpan? minInterval = null)
Subscribe variant of AddResize returning a disposable subscription handle.
RemoveResize
ValueTask<Guid[]> RemoveResize(Action handler)
Removes resize listeners matched by delegate identity; pass the exact handler instance that was registered.
RemoveResize
ValueTask RemoveResize(Guid id)
Removes a single resize listener by its id.
AddScroll
ValueTask<Guid> AddScroll(Action handler, TimeSpan? minInterval = null)
Registers a handler for the scroll event and returns its listener id. minInterval rate-limits the handler in JavaScript, before the round trip.
SubscribeScroll
ValueTask<ButilSubscription> SubscribeScroll(Action handler, TimeSpan? minInterval = null)
Subscribe variant of AddScroll returning a disposable subscription handle.
RemoveScroll
ValueTask<Guid[]> RemoveScroll(Action handler)
Removes scroll listeners matched by delegate identity; pass the exact handler instance that was registered.
RemoveScroll
ValueTask RemoveScroll(Guid id)
Removes a single scroll listener by its id.
RemoveAllEventHandlers
ValueTask RemoveAllEventHandlers()
Removes every resize and scroll listener registered on this instance.
InvokeVisualViewport
void InvokeVisualViewport(Guid id)
JS interop bridge invoked by the browser on resize/scroll events; not intended for application code.
DisposeAsync
ValueTask DisposeAsync()
Removes all listeners and releases the interop reference.
SubscribeScrollEnd
ValueTask<ButilSubscription> SubscribeScrollEnd(Action handler)
Fires once when a visual-viewport scroll settles rather than on every frame of it. Prefer this over SubscribeScroll for expensive work. Not implemented in Safari, where the handler never fires.
An unhandled error has occurred. Reload 🗙