Three weeks ago I redid my mobile viewports tests for the umpteenth time, and today I give you my overview. There is some progress to report, but also a lot of changes.
screen.width/heightnow gives the dimensions of the ideal layout viewport in nearly all mobile browsers.
getBoundingClientRect()is relative to the visual viewport in most browsers, but relative to the layout viewport in Chromium 50+ and Edge.
Touch/Click.clientX/Yhas the same problem: it is relative to the visual viewport in most browsers, but relative to the layout viewport in Chromium 45+ and Edge.
Touch/Click.screenX/Yis still chaotic. Don’t use.
Before reading this post I advise you to take a good look at my viewports visualisation app. Pay close attention to how the layout viewport moves, and remember that a movable layout viewport is a relatively recent innovation supported only by Chromium 40+ or thereabouts, and Edge. In the other browsers the layout viewport is simply the top of the document.
To start with the good news: the screen.width problem I discovered three years ago — where some mobile browsers return the physical screen size and others the ideal layout viewport size — is being done away with.
screen.width/height now gives the ideal viewport dimensions in most browsers.
The only hold-outs for the old, physical definition are Android WebKit, whose development has ceased, and UC, which is still very much under development but suffers from a lack of communication (my old contact person disappeared).
In addition, Edge on mobile makes a mess of things by, apparently, making
screen.width/height equal to the actual layout viewport size. I’m assuming that this is in fact a bug, and not a third definition. (Also, on desktop these properties are quite a bit more complicated than on mobile. We won’t go into that.)
The other properties remain the same as always, and are pretty well supported:
window.innerWidth/Heightto keep exposing the visual viewport dimensions for all eternity. Let’s remain watchful here and protest again if Google goes back to its old tricks.
I wasted way too much time on testing the three touch/click coordinate pairs:
screenX/Y. So I’ll waste your time as well. Good news first.
Touch/Click.pageX/Y gives the coordinates relative to the HTML document in all browsers. About 95% of the time, this is what you want to know.
pageX/Y uses the same coordinate system as
position: absolute, so if you copy the
pageX/Y coordinates to the
style.left/top of an absolute element, that element will appear at the exact point the user clicked or touched.
I advise you to always use
pageX/Y, unless you’re very certain that you want something else AND fully understand the finicky browser problems that follow.
Touch/Click.clientX/Y has acquired a new mobile definition. In older browsers it means relative to the visual viewport; in newer ones (basically Chrome 40+ or thereabouts) it means relative to the layout viewport.
Keep in mind that in older browsers the layout viewport does not move, so that coordinates relative to the layout viewport are the same as coordinates relative to the document.
Touch/Click.screenX/Y on mobile is still in chaos. Don’t use.
screenX/Y may mean
clientX/Y, the coordinates relative to the physical screen, or it might just be 0. Or buggy.
In modern Chromia (again from about 40 onward), however, it means coordinates relative to the screen, but in CSS pixels, not in physical device pixels. I’m not totally sure if this is going to be the final definition, but in some cases it might be useful knowledge.
getBoundingClientRect() returns, as MDN clearly states, “the size of an element, and its position relative to the viewport.” Respectively, these measurements are found in
That sounds great, until you ask the obvious question: relative to WHICH viewport is the position measured? MDN doesn’t say. (Did I ever tell you to distrust any spec or technical document that doesn’t specify which viewport it means? Well, you should.) As to the spec, it doesn’t even mention the
The usual ill-defined incompatibility soup, in other words. Fortunately I figured it out for you:
getBoundingClientRect() uses the visual viewport as reference, although Chromium 50+ and Edge use the layout viewport instead.
The consequences are subtle. In Chromium 50+ and Edge it’s possible to get the current offset of the layout viewport — and note the negative sign at the start. (Many thanks to Rick Byers for pointing out this trick.)
In other browsers this will misfire: it will give you the offset of the visual viewport instead. Fortunately you can easily detect that: if the expression returns the same values as
window.pageX/YOffset you’re dealing with an older browser that doesn’t move the layout viewport, and you should assume the offset is 0,0 or undefined, whichever works best for you.
This overview shows that, strange as it may sound, there are too few viewport-related properties. Specifically, if we accept the latest Google-pushed changes I’d say we also need the following:
screen.availHeight, which would be
screen.heightminus the currently visible browser and OS toolbars. (We can throw in
availWidthas well, but no mobile browser uses left or right toolbars.)
Google has a visual viewport API proposal (see also discussion and more discussion). It’s not a bad idea, although I find the proposed
scrollTop/Left, which return the scrolling offset of the visual viewport relative to the layout viewport, a bit weird and unlikely to be of interest to web developers.
The problem is not the
scrollTop/Left definition, but the fact that there’s no similar API for the layout viewport. Ideally, we’d have the same API for both, and both would measure relative to the document.
If I understand correctly (but this is older information), Google’s plan is to change all legacy properties (i.e. the ones I tested just now) as layout viewport properties, while the new API would handle the visual viewport.
I strongly disagree. I’d say we have to leave the old properties intact for cross-browser and backward compatibility (they’re being used in the wild; and not all browsers will implement the new APIs). We should add two new APIs, one for each of the viewports. Then we’d have several property pairs giving the same information, which is a bit odd but not unprecedented. More importantly, we’d have a logical framework for all visual and layout viewport dimensions while at the same time preserving compatibility.
I also retested the meta viewport itself and the possibilities we have for changing it after the initial parsing of the HTML. Spoiler: you can do so by simply rewriting the meta viewport tag:
var vp = [the meta viewport tag]; vp.setAttribute('content','width=400');
The only thing that is not possible is removing the meta viewport altogether.
Still, I found two incompatibilities in WebViews. While neither is a serious problem, I think this is the first time WebView incompatibilities go under their own heading. Also, they prove a WebView is not necessarily the same as the browser it’s supposed to be based on.
The more important one: the Chromium WebView does not support the meta viewport. At all. Instead, it always acts as if
width=device-width is set, even if you explicitly set another value or leave out the tag entirely. Firt told me you can set a viewport property in the app that calls the WebView, which helps. Still, I hadn’t expected this result.
A less important one: the Android WebKit WebView does not allow you to rewrite the meta viewport tag, although it obeys the one that’s hard-coded in your HTML. This is a difference with the Android WebKit browser, which does allow the rewriting of the meta viewport.
I also found a way of calculating the combined height of the browser and system toolbars in some situations in most browsers. It’s not entirely clear to me if this is old news or not, but here it goes anyway:
var toolbarOffset = screen.height - (window.innerHeight * (document.documentElement.clientWidth / window.innerWidth))
Take the total screen height (which is equal to the ideal layout viewport height) and subtract from it the visual viewport height times the current zoom factor. This zoom factor is calculated using widths, not heights, because widths are never changed by toolbars (at least, not in any of the many browsers I tested).
Unfortunately this calculation rests on two assumptions:
width=device-width. Maybe I’ll devise a way of getting around this restriction, but I have spent too much time on this already.
screen.heightgives the ideal viewport height. Therefore this will not work in Android WebKit, UC, and Edge.
So use with caution. It might work. Then again, it might not.
I’m around at the following conferences: