New CSS Viewport Units Do Not Solve The Classic Scrollbar Problem — Smashing Magazine

New CSS Viewport Units Do Not Solve The Classic Scrollbar Problem width: 100vw as a way to set an element’s width to the full width of the viewport. But, as Šime Vidas explains in this deep dive, 100vw does not always represent the full width of the viewport due to differences in how browsers handle scrollbars. Learn why this is an issue, how to avoid it, and what approaches we may have to completely solve it in the future. Browsers shipped a new set of CSS viewport units in 2022. These units make it easier to size elements in mobile browsers, where the browser’s retractable UI affects the height of the viewport as the user scrolls the page. Unfortunately, the new units do not make it easier to size elements in desktop browsers, where classic scrollbars affect the width and height of the viewport. The following video shows a desktop browser with classic scrollbars. As we resize the viewport (dashed line) in different ways, the CSS length 100dvw matches the width of the viewport in all situations except when a vertical classic scrollbar is present on the page. In that case, 100dvw is larger than the viewport width. This is the classic scrollbar problem of CSS viewport units. When the page has a vertical classic scrollbar, the length 100dvw is larger than the viewport width. In fact, all viewport units have this problem. Before discussing the solutions and workarounds to the classic scrollbar problem, we should familiarize ourselves with all the relevant concepts, which include the visual and layout viewport , the two types of zoom , the initial containing block , the original and new viewport units , and the two types of scrollbars . The Visual And Layout Viewports The viewport is the rectangular section of the web browser in which the web page is rendered. For example, when a web page loads in Safari on an iPhone SE, the viewport has a width of 375 CSS pixels 1 and a height of 548 CSS pixels. This size is called the “small viewport size” . If the user then scrolls the page, causing the browser’s UI to retract, the height of the viewport increases to 626 CSS pixels, an additional 78 pixels. This size is called the “large viewport size” . 1 The width and height of the viewport are measured in CSS pixels, not device pixels. On most modern displays, especially on mobile devices, one CSS pixel consists of two or more device pixels. If the user rotates their device and the operating system switches to landscape mode, the size of the viewport changes (it becomes wider than it is tall), but there is again a small and large viewport size. In desktop browsers, the size of the viewport can change as well (e.g., when the user resizes the browser window, opens the browser’s sidebar, or zooms the page), but there is no separate “small viewport size” and “large viewport size” like in mobile browsers. So far, I’ve only talked about the “viewport,” but there are, in fact, two different viewports in web browsers: the visual viewport and the layout viewport . When the page initially loads in the browser, the visual viewport and the layout viewport have the exact same size and position. The two viewports diverge in the following two cases: When the user zooms in on a part of the page via a pinch-to-zoom or double-tap gesture, the part of the page that is visible on the screen is the visual viewport. The size of the visual viewport (in CSS pixels) decreases because it shows a smaller part of the page. The size of the layout viewport has not changed. When the browser’s virtual keyboard appears on mobile platforms, the smaller part of the page that is visible on the screen above the keyboard is once again the visual viewport. The height of the visual viewport decreases with the height of the virtual keyboard. The size of the layout viewport has again not changed. It’s worth noting that as part of shipping the new viewport units in 2022, Chrome stopped resizing the layout viewport and initial containing block (ICB) when the virtual keyboard is shown. This behavior is considered the “the best default”, and it ensures that the new viewport units are consistent across browsers. This change also made the mobile web feel less janky because resizing the ICB is a costly operation. However, the virtual keyboard may still resize the layout viewport in some mobile browsers. In these two cases, the visual viewport continues to be “the rectangular section of the web browser in which the web page is rendered,” while the layout viewport becomes a larger rectangle that is only partially visible on the screen and that completely encloses the visual viewport. In all other situations, both viewports have the same size and position. One benefit of the two-viewport system is that when the user pinch-zooms and pans around the page, fixed-positioned elements don’t stick to the screen, which would almost always be a bad experience. That being said, there are valid use cases for positioning an element above the virtual keyboard (e.g., a floating action button). The CSS Working Group is currently discussing how to make this possible in CSS. CSS viewport units are based on the layout viewport, and they are unaffected by changes to the size of the visual viewport. Therefore, I will focus on the layout viewport in this article. For more information about the visual viewport, see the widely supported Visual Viewport API. The Two Types Of Zoom The two types of zoom are defined in the CSSOM View module: “There are two kinds of zoom: page zoom, which affects the size of the initial viewport, and the visual viewport scale factor, which acts like a magnifying glass and does not affect the initial viewport or actual viewport.” Page zoom is available in desktop browsers, where it can be found in the browser’s menu under the names “Zoom in” and “Zoom out” or just “Zoom”. When the page is “zoomed in,” the size of the layout viewport shrinks, which causes the page to reflow. If the page uses CSS media queries to adapt to different viewport widths (i.e., responsive web design), those media query breakpoints will be triggered by page zoom. Scale-factor zoom is available on all platforms. It is most commonly performed with a pinch-to-zoom gesture on the device’s touch screen (e.g., smartphone, tablet) or touchpad (e.g., laptop). As I mentioned in the previous section, the size of the layout viewport does not change when zooming into a part of the page, so the page does not reflow. Page Zoom Visual Viewport Scale Factor Available on Desktop platforms All platforms Activated by Keyboard command, menu option Pinch-to-zoom or double-tap gesture Resizes Both layout and visual viewport Only visual viewport Does it cause reflow? Yes No The Layout Viewport And The Initial Containing Block The layout viewport is the “containing block” for fixed-positioned elements. In other words, fixed-positioned elements are positioned and sized relative to the layout viewport. For this reason, the layout viewport can be viewed as the “position fixed viewport,” which may even be a better name for it. /* this element completely covers the layout viewport */
.elem {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Here’s a tip for you : Instead of top: 0 , bottom: 0 , left: 0 , and right: 0 in the snippet above, we can write inset: 0 . The inset property is a shorthand property for the top , bottom , left , and right properties, and it has been supported in all major browsers since April 2021. The initial containing block (ICB) is a rectangle that is positioned at the top of the web page. The ICB has a static size, which is the “small viewport size.” When a web page initially loads in the browser, the layout viewport and the ICB have the exact same size and position. The two rectangles diverge only when the user scrolls the page: The ICB scrolls out of view, while the layout viewport remains in view and, in the case of mobile browsers, grows to the “large viewport size.” The ICB is the default containing block for absolutely positioned elements. In other words, absolutely positioned elements are, by default, positioned and sized relative to the ICB. Since the ICB is positioned at the top of the page and scrolls out of view, so do absolutely-positioned elements. /* this element completely covers the ICB by default */
.elem {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
The ICB is also the containing block for the element itself, which is the root element of the web page. Since the ICB and the layout viewport initially have the same size (the “small viewport size”), authors can make the element as tall as the initial viewport by setting height to 100% on both the and element. /* By default: ICB height = initial viewport height */

/* height = ICB height */
html {
height: 100%;
}

/* height = height */
body {
margin: 0;
height: 100%;
}

/* Result: height = initial viewport height */
Some websites, such as Google Search, use this method to position the page footer at the bottom of the initial viewport. Setting height to 100% is necessary because, by default, the and elements are only as tall as the content on the page. Layout Viewport Initial Containing Block Containing block for position: fixed elements position: absolute elements Is it visible? Always in view (at least partially 2 ). Scrolls out of view (positioned at the top of the page). Size Small or large viewport size (depending on the browser UI) Small viewport size 2 The layout viewport is not fully visible when the user zooms in on the part of the page and when the browser’s virtual keyboard is shown. The New Viewport Units The CSS viewport units are specified in the CSS Values and Units Module, which is currently at Level 4. The original six viewport units shipped in browsers a decade ago. The new units shipped in major browsers over the past year, starting with Safari 15.4 in May 2022 and ending with Samsung Internet 21 in May 2023. Note : The new viewport units may not be correctly implemented in some mobile browsers. Layout Viewport Original Units (2013) New Unit Equivalent (2022) Width vw svw , lvw , dvw Height vh svh , lvh , dvh Inline Size vi svi , lvi , dvi Block Size vb svb , lvb , dvb Smaller Size vmin svmin , lvmin , dvmin Larger Size vmax svmax , lvmax , dvmax A few clarifications: The “inline size” and “block size” are either the width or the height, depending on the writing direction. For example, in writing systems with a left-to-right writing direction, the inline size is the width ( vi is equivalent to vw ), and the block size is the height ( vb is equivalent to vh ). The ”smaller size” and “larger size” are either the width or the height, depending on which one is larger. For example, if the viewport is rather tall than it is wide (e.g., a smartphone in portrait mode), then the smaller size is the width ( vmin is equivalent to vw ), and the larger size is the height ( vmax is equivalent to vh ). Each viewport unit is equal to one-hundredth of the corresponding viewport size. For example, 1vw is equal to one-hundredth of the viewport width, and 100vw is equal to the entire viewport width. For each of the six original units, there are three new variants with the prefixes s , l , and d (small, large, and dynamic, respectively). This increases the total number of viewport units from 6 to 24. The s -prefixed units represent the “small viewport size.” This means that 100svh is the height of the initial layout viewport when the browser’s UI is expanded. The l -prefixed units represent the “large viewport size.” This means that 100lvh is the height of the layout viewport after the browser’s UI retracts. The height difference between the large and small viewport sizes is equivalent to the collapsible part of the browser’s UI: 100lvh – 100svh = how much the browser’s UI retracts The old unprefixed units (i.e., , which means that they also represent the “large viewport size.” For example, vw , vh , and so on) are equivalent to the l -prefixed units in all browsers 100vh is equivalent to 100lvh . The which can be either the “small viewport size” or the “large viewport size.” This means that d -prefixed units represent the current size of the layout viewport, 100dvh is the actual height of the layout viewport at any given point in time. This length changes whenever the browser’s UI retracts and expands. Why Do We Have New CSS Units? In previous years, the Android version of Chrome would resize the vh unit whenever the browser’s UI retracted and expanded as the user scrolled the page. In other words, vh behaved like dvh . But then, in February 2017, Chrome turned vh into a static length that is based on the “largest possible viewport”. In other words, vh started behaving like lvh . This change was made in part to match Safari’s behavior on iOS, which Apple implemented as a compromise: “Dynamically updating the [ 100vh ] height was not working. We had a few choices: drop viewport units on iOS, match the document size like before iOS 8, use the small view size, or use the large view size. From the data we had, using the larger view size was the best compromise. Most websites using viewport units were looking great most of the time.” With this change in place, the same problem that occurred in iOS Safari also started happening in Chrome. Namely, an element with height: 100vh , which is now the “large viewport size,” is taller than the initial viewport, which has the “small viewport size.” That means the bottom part of the element is not visible in the viewport when the web page initially loads. This prompted discussions about creating a solution that would allow authors to size elements based on the small viewport size. One of the suggestions was an environment variable, but the CSS Working Group ultimately decided to introduce a new set of viewport units. /* make the hero section as tall as the initial viewport */
.hero {
height: 100svh;
}
The same height can be achieved by setting height to 100% on the .hero element and all its ancestors, including the and elements, but the svh unit gives authors more flexibility. I wasn’t able to find any good use cases for the dvh unit. It seems to me that sizing elements with dvh is not a good idea because it would cause constant layout shifts as the user scrolled the page. I had considered dvh for the following cases: For fixed-positioned elements , such as modal dialogs and sidebars, height: 100% behaves the same as height: 100dvh because the containing block for fixed-positioned elements is the layout viewport, which already has a height of 100dvh . In other words, height: 100% works because 100% of 100dvh is 100dvh . This means that the dvh unit is not necessary to make fixed-positioned elements full-height in the dynamic viewport of mobile browsers. For vertical scroll snapping , setting the individual “pages” to height: 100dvh results in a glitchy experience in mobile browsers. That being said, it is entirely possible that mobile browsers could fix this issue and make scroll snapping with height: 100dvh a smooth experience. There is no concept of a “small viewport size” and a “large viewport size” in desktop browsers. All viewport units, new and old, represent the current size of the layout viewport, which means that all width units are equivalent to each other (i.e., vw = svw = lvw = dvw ), and all height units are equivalent to each other (i.e., vh = svh = lvh = dvh ). For example, if you replaced 100vh with 100svh in your code, nothing would change in desktop browsers. This behavior isn’t exclusive to desktop platforms. It also occurs on mobile platforms in some cases, such as when a web page is embedded in an