Viewport Units • CSS for JavaScript Developers
Modern web applications respond dynamically, contorting themselves to display well across an incredibly wide range of devices. In this module, we’ll learn how to write CSS that responds and adapts to different situations.
Did you know that CSS has types?
Every value that you might think to use, like 24px or 10% or #FF0000, has a type. It might be a <length>
or a <color>
or an <angle>
, or one of many other possible types.
Our CSS properties, meanwhile, accept values of specific types. background-color
accepts a <color>
, so hsl(0deg 100% 50%)
is a valid value, but 100%
isn’t.
The <length>
type is one of the most common. Properties like width
or padding
accept <length>
values, and it contains units like px and em and rem, and more obscure ones we’ll cover later, like ch.
It also includes viewport units, the subject of this lesson.
There are two main viewport units: vw
(Viewport Width) and vh
(Viewport Height).
1vw
is equivalent to 1% of the viewport width. For example:
.box {
width: 10vw;
height: 25vh;
}
We can use these units with width
and height
, but the really cool thing is that we can use them with any property that accepts <length>
units.
In this playground, we use it to increase the distance between letters:
Viewport units have excellent browser support: all major browsers, and even Internet Explorer, all the way back to IE 9!
The mobile height issue
The vh
unit in particular is often used to solve one annoying problem: making sure that an element is exactly as tall as the viewport. No taller, no shorter.
Unfortunately, it doesn’t quite work as you’d expect on mobile. To understand why, we need to look at how modern mobile browsers work.
When a page is loaded on a mobile browser, it includes an “expanded” browser UI: the address bar is tall and tappable, and an array of buttons line up along the bottom. Once you start scrolling, though, the browser UI slips away, making more space for the content:
The screenshots are from Safari on my iPhone X, but Chrome on Android has a similar effect.
When Apple first introduced this “slide-away” UI feature, the vh
unit was dynamic: it would grow to match the viewport height when the UI slid away. This led to some really bad experiences, though: having elements shift and resize when you start scrolling is unexpected, and led to some very janky experiences.
So nowadays, the vh
unit always refers to the largest possible height. In our example above, 100vh
will always equal 750px, even when the page first loads, and the viewport is actually only 635px tall.
If you set an element to have 100vh
, therefore, it won’t fit on the screen:
You can try this demo yourself, on your mobile device, at this URL:
How do we work around this? We have a few options:
-
We can use a JS-based solution to change how the
vh
unit works. The most popular solution is viewport-units-buggyfill. Personally, I wouldn’t recommend this unless you really need thevh
unit to work. Even if it works perfectly today, it could break when browsers update (and imagine how hard it would be to trace that bug!) -
We can tweak our designs so that they don’t need to fill the viewport exactly. Fixed-height designs tend to be rigid and flaky; better to have a fluid design that doesn’t have such a strict height requirement.
The vh
unit can still come in handy, but it probably shouldn’t be used in this exact situation.
Unfortunately, vh
isn’t alone in having some problems: the vw
unit isn’t perfect either!
Here’s the problem: vw
refers to the viewport width not counting the scrollbar.
On mobile, this is fine, because the scrollbar floats transparently above the content. On desktop, though, the scrollbar usually takes up its own space, within the viewport. The exact width depends on the platform and on the styling.
This means that if we set an element to stretch to 100vw
, and our scrollbar is 15px wide, we’ll wind up with 15px of horizontal overflow:
You can also view a live example of this issue, though the issue will only be present on desktop (on mobile, scrollbars don’t take up any width).
Can we fix this? Sorta, but not really. 😬
In an earlier version of this lesson, I included a JS snippet that would dynamically measure the width of the scrollbar, and make it available to us through a CSS variable. Since publishing this lesson, though, I’ve come to realize that it’s not the best idea. This is for several reasons:
-
If you’re using “server-side rendering” in a JS framework like React, there will be a delay before the JS runs, meaning that the value won’t be defined at first, potentially breaking the layout.
-
If the page loads without a scrollbar, but later introduces one (eg. after fetching data from the network), the scrollbar width won’t be defined.
-
While it seems to work pretty well in most situations I’ve tried, there are probably more edge-cases I haven’t run into.
This is a bummer, but honestly, I don’t really find the vw
unit super useful. As we learned about in Module 1, block-level elements will expand horizontally, making it much easier to use percentage-based widths compared to percentage-based heights.
The one common use case I’ve seen for vw
is to “break out” of parent containers, but I have a grid-based approach that feels much less hacky to me.
I’ll include my JS snippet below, but please use it with caution:
vmin and vmax
There are two more nifty viewport units: vmin
and vmax
.
vmin
will refer to the shorter dimension, while vmax
refers to the longer one. On a portrait phone, 50vmin
is equivalent to 50vw
, but on a landscape monitor, 50vmin
would be equal to 50vh
.
These units are nifty, but honestly, I don’t find myself reaching for them that often. If they have practical everyday use cases, I haven’t discovered them yet.