Chrome nettleser, Nyheter

Introducing visualViewport

Introducing visualViewport

What if I told you, there’s more than one viewport.

BRRRRAAAAAAAMMMMMMMMMM

And the viewport you’re using right now, is actually a viewport within a
viewport.

BRRRRAAAAAAAMMMMMMMMMM

And sometimes, the data the DOM gives you, refers to one of those viewport and
not the other.

BRRRRAAAAM… wait what?

It’s true, take a look:

Layout viewport vs visual viewport

The video above shows a web page being scrolled and pinch-zoomed, along with a
mini-map on the right showing the position of viewports within the page.

Things are pretty straight forward during regular scrolling. The green area
represents the layout viewport, which position: fixed items stick to.

Things get weird when pinch-zooming is introduced. The red box represents the
visual viewport, which is the part of the page we can actually see. This
viewport can move around while position: fixed elements remain where they
were, attached to the layout viewport. If we pan at a boundary of the layout
viewport, it drags the layout viewport along with it.

Improving compatibility

Unfortunately web APIs are inconsistent in terms of which viewport they refer
to, and they’re also inconsistent across browsers.

For instance, element.getBoundingClientRect().y returns the offset within the
layout viewport. That’s cool, but we often want the position within the page,
so we write:

element.getBoundingClientRect().y + window.scrollY

However, many browsers use the visual viewport for window.scrollY, meaning
the above code breaks when the user pinch-zooms.

Chrome 61 changes window.scrollY to refer to the layout viewport instead,
meaning the above code works even when pinch-zoomed. In fact, browsers are
slowly changing all positional properties to refer to the layout viewport.

With the exception of one new property…

Exposing the visual viewport to script

A new API exposes the visual viewport as window.visualViewport. It’s a draft
spec
, with cross-browser
approval
, and it’s
landing in Chrome 61.

console.log(window.visualViewport.width);

Here’s what window.visualViewport gives us:

visualViewport properties
offsetLeft Distance between the left edge of the visual viewport, and the layout
viewport, in CSS pixels.
offsetTop Distance between the top edge of the visual viewport, and the layout
viewport, in CSS pixels.
pageLeft Distance between the left edge of the visual viewport, and the left
boundary of the document, in CSS pixels.
pageTop Distance between the top edge of the visual viewport, and the top
boundary of the document, in CSS pixels.
width Width of the visual viewport in CSS pixels.
height Height of the visual viewport in CSS pixels.
scale The scale applied by pinch-zooming. If content is twice the size due to
zooming, this would return 2. This is not affected by
devicePixelRatio.

There are also a couple of events:

window.visualViewport.addEventListener('resize', listener);
visualViewport events
resize Fired when width, height, or
scale changes.
scroll Fired when offsetLeft or offsetTop changes.

Demo

The video at the start of this article was created using visualViewport,
check it out in Chrome 61+. It uses
visualViewport to make the mini-map stick to the top-right of the visual
viewport, and applies an inverse scale so it always appears the same size,
despite pinch-zooming.

Gotchas

Events only fire when the visual viewport changes

It feels like an obvious thing to state, but it caught me out when I first
played with visualViewport.

If the layout viewport resizes but the visual viewport doesn’t, you don’t get a
resize event. However, it’s unusual for the layout viewport to resize without
the visual viewport also changing width/height.

The real gotcha is scrolling. If scrolling occurs, but the visual viewport
remains static relative to the layout viewport, you don’t get a scroll event
on visualViewport, and this is really common. During regular document
scrolling, the visual viewport stays locked to the top-left of the layout
viewport, so scroll does not fire on visualViewport.

If you’re wanting to hear about all changes to the visual viewport, including
pageTop and pageLeft, you’ll have to listen to the window’s scroll event
too:

visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
window.addEventListener('scroll', update);

Avoid duplicating work with multiple listeners

Similar to listening to scroll & resize on the window, you’re likely to call
some kind of «update» function as a result. However, it’s common for many of
these events to happen at the same time. If the user resizes the window, it’ll
trigger resize, but quite often scroll too. To improve performance, avoid
handling the change multiple times:

// Add listeners
visualViewport.addEventListener('scroll', update);
visualViewport.addEventListener('resize', update);
addEventListener('scroll', update);

let pendingUpdate = false;

function update() {
  // If we're already going to handle an update, return
  if (pendingUpdate) return;

  pendingUpdate = true;

  // Use requestAnimationFrame so the update happens before next render
  requestAnimationFrame(() => {
    pendingUpdate = false;

    // Handle update here
  });
}

I’ve filed a spec issue for
this
, as I think there may be a
better way, such as a single update event.

Event handlers don’t work

Due to a Chrome
bug
, this does
not work
:

Buggy – uses an event handler

visualViewport.onscroll = () => console.log('scroll!');

Instead:

Works – uses an event listener

visualViewport.addEventListener('scroll', () => console.log('scroll'));

Offset values are rounded

I think (well, I hope) this is another Chrome
bug
.

offsetLeft and offsetTop are rounded, which is pretty inaccurate once the
user has zoomed in. You can see the issues with this during the
demo
– if the user zooms in and pans
slowly, the mini-map snaps between unzoomed
pixels
.

The event rate is slow

Like other resize and scroll events, these no not fire every frame,
especially on mobile. You can see this during the
demo
– once you pinch zoom, the mini-map
has trouble staying locked to the viewport.

Accessibility

In the demo I used visualViewport to
counteract the user’s pinch-zoom. It makes sense for this particular demo, but
you should think carefully before doing anything that overrides the user’s
desire to zoom in.

visualViewport can be used to improve accessibility. For instance, if the user
is zooming in, you may choose to hide decorative position: fixed items, to get
them out of the user’s way. But again, be careful you’re not hiding something
the user is trying to get a closer look at.

You could consider posting to an analytics service when the user zooms in. This
could help you identify pages that users are having difficulty with at the
default zoom level.

visualViewport.addEventListener('resize', () => {
  if (visualViewport.scale > 1) {
    // Post data to analytics service
  }
});

And that’s it! visualViewport is a nice little API which solves compatibility
issues along the way.

This post is also available in: English

author-avatar

About Aksel Lian

En selvstendig full stack webutvikler med en bred variasjon av kunnskaper herunder SEO, CMS, Webfotografi, Webutvikling inkl. kodespråk..