Preventing vh jump on mobile browsers

When building a responsive layout in CSS, one way to span the a fraction of the viewport is to use viewport-relative units, namely vh, vw. These units are often used when designing a magazine-like page with full-page sections. A typical example would be:

.hero {
    /* spans the height of viewport */
    height: 100vh;
}

On smaller screens, to increase screen real estate, developers started to hide address bar on scroll down, and show it back on scroll up. This has brought on the issue that after hiding or showing the bar, viewport height is recalculated and layout is updated accordingly.

Consequently, due to elements with height or min-height specified in vh units, end users started to experience jumps in scroll position. With one or two elements, the jump is not pronounced, but if the user is closer to the end of the page, and large number of elements employ viewport units, length of the jump increases dramatically, not to mention CPU-intensive layout calculations, and paint jobs.

Method 1: Using JS to replace sizes with pixel units

One way to overcome this problem is to convert vh units to pixels using JS. You can easily simply add a class .js-fix-height to elements that need fixing, and override CSS declarations with pixel equivalents with:

const fixables = document.querySelectorAll('.js-fix-height');
[].forEach.call(fixables, (el) => {
    const box = el.getBoundingClientRect();
    // round height to nearest pixel
    el.style.height = box.height.toFixed() + 'px';
});

You can see a demo with JS here

One thing to note is that calling getBoundingClientRect() on each element will force browser to calculate each element's style and and layout, which might prove costly and cause stutters on low-end devices if you're calling it on large number of elements.

Method 2: Using CSS to delay size change

A better solution to this would be ditching JS for CSS to fix it once and for all using transitions. Now you might be inclined to say, "Whoa, but using transition for height causes reflow and repaint on each frame!", and you would be right, but only partially. What you're forgetting is transition-timing-function property. Now consider this:

.box {
    width: 10rem;
    height: 10rem;
    transition: height 1s steps(1);
}
/* double the size when body is hovered */
body:hover .box {
    height: 20rem;
}

By specifying transition-timing-function as steps(1)1, we just reduced number of intermediate calculations to zero (transition completes in a single step, with no state in between). Now the trick is to delay this change long enough that it does not happen at all on a typical viewing period. Using transition-duration or transition-delay does not matter, and both values are effectively combined when steps(1) is specified. I prefer refactoring this to a utility class.

.hero {
    min-height: 100vh;
}
.u-prevent-jump {
    transition: min-height 1000s steps(1);
}
<div class="hero u-prevent-jump">
    Hero content
</div>

You can see a demo with CSS here

Now the hero element height does not respond to viewport height changes for more than 16 minutes, which prevents undesired jumps very effectively.


  1. You can also use step-start or step-end, both of which has the same effect as steps(1), but I find steps function easier to read. ⚓︎