DEV Community

Vladimir Elchinov for Session Replay

Posted on

"I Can't Reach the Button" Is Unreproducible on Your Desktop, and `100vh` Is Why

A report comes in: the submit button at the bottom of a full-height section cannot be tapped. You open the page on your desktop. Fine. You open the responsive emulator at the phone's exact dimensions. Also fine. You close the ticket.

The reporter was right, and so were you. The difference is a browser interface neither of you was looking at.

vh is not the height of the screen

It is the height of the screen with the URL bar taken away. Not as a bug, and not as an implementation detail somebody will fix. It is what the unit is specified to mean.

CSS Values and Units Level 4 defines three viewport sizes. The small viewport is "the viewport sized assuming any UA interfaces that are dynamically expanded and retracted to be expanded". The large viewport is the same sentence with the last word changed to "retracted". And then the part that catches people:

The large viewport-percentage units (lv*) and default viewport-percentage units (v*) are defined with respect to the large viewport size.

The default units. vh is lvh. Your 100vh section is laid out as though the URL bar is not there, on a device where it is there until the user scrolls.

So on first paint, before anybody has scrolled, the bottom of that section is underneath the browser chrome. The button is on the page, it is rendered, it has non-zero dimensions, and your tests will say so. It is just not somewhere a thumb can go.

Why it was specified that way

Not an oversight. The alternative is worse.

If vh tracked the bar as it retracted, every full-height element would reflow mid-scroll. Headers would jump, a sticky footer would walk up the screen, and text would reparagraph under the reader's eyes while they were reading it. Pinning vh to the larger of the two sizes buys you a unit that holds still. The cost is that it is wrong by the height of the bar at exactly the moment most people see your page for the first time.

You can see all three at once:

const probe = document.createElement('div');
probe.style.position = 'absolute';   // out of flow, so measuring cannot move the page
probe.style.visibility = 'hidden';
document.body.append(probe);

for (const unit of ['vh', 'svh', 'lvh', 'dvh']) {
  // An unsupported unit is an invalid declaration, which leaves the previous
  // value in place and would print the last unit's number as if it were this one.
  if (!CSS.supports('height', `100${unit}`)) {
    console.log(unit, 'not supported here');
    continue;
  }
  probe.style.height = `100${unit}`;
  console.log(unit, probe.getBoundingClientRect().height);
}

probe.remove();
console.log('innerHeight', window.innerHeight);
console.log('visualViewport', window.visualViewport?.height);
Enter fullscreen mode Exit fullscreen mode

On a desktop browser every number is the same, which is the entire reason this never shows up while you are working. On a phone, before you scroll, svh and lvh differ by the height of the bar and vh sits with lvh.

The fix you reach for is the one the spec warns about

Having found dvh, the obvious move is to swap it in everywhere. The specification gets ahead of you:

The sizes of the dynamic viewport-percentage units are not stable even while the viewport itself is unchanged. Using these units can cause content to resize e.g. while the user scrolls the page.

That is the reflow problem you just opted back into. dvh is right where a resize mid-scroll is genuinely harmless. For anything that has to be reachable without scrolling - a primary action, a dismiss control, a cookie banner's accept button - svh is the honest unit, because it is the one that assumes the chrome is in the way. The layout is slightly short when the bar retracts. Nobody has ever filed a ticket about slightly short.

What the report could not have told you

Here is the part that generalises past this one unit.

Ask the reporter what their viewport was and they will send you a screen size, because that is the number a phone knows about itself. It is the wrong number. What decided the layout was whether a piece of browser furniture was expanded at that moment, which is not a device property, not a setting, and not something they can observe. It depends on whether they arrived from a link or a home screen icon, and on whether they had scrolled yet.

A screenshot does not settle it either. The screenshot shows the visual viewport. Your CSS was reasoning about the large one. Those are the two numbers whose disagreement is the whole bug, and the image contains only one of them.

Which is the shape of most unreproducible reports, once you look at them: the fact that would decide it is not hidden, it is just not a fact the person in front of the problem is able to know they should write down.

Top comments (1)

Collapse
 
devenquan profile image
Zhang Enquan •

The same shape of bug bit me from the other direction, and it's worth naming because it's the same failure mode as 100vh.

A tool that worked perfectly locally and was completely broken in production. Same file, same code. WordPress had rewritten every & inside my into &#038; on the way from the editor to the page. Two hours to find it. The diff was one character.</p> <p>What yours and mine share: both are invisible in the environment you test in and load-bearing in the one users actually get. Yours needs a real phone with a real URL bar; mine needed a real CMS render. &quot;Works on my machine&quot; survives precisely because the machine is the one place the transform doesn&#39;t run.</p>