CSS has all kinds of units that you can use: px
, em
, rem
, ex
, cm
, etc. The list is long, and it's easy to think that they're all interchangeable, but they're not! To highlight some of the differences, let's focus on a gotcha when using one of the newer units -- the viewport width (vw
).
The viewport is the part of a web page that is currently visible to the user. Its width is defined to be exactly 100vw. So if an element has a width of 10vw, it'll take up 10% of the window's width. This is slightly different from percentage widths, where an element with a width of 10% will take up 10% of its parent element's width, which may or may not be equal to the entire window's width.
The vw
unit is terrific, simplifying the implementation of responsive layouts, but then, when you least expect it...
The Gotcha
Content with a width of 100vw sometimes creates a tiny unwanted horizontal scroll. This is odd because, by definition, 100vw is the full viewport width, so content shouldn't spill past that.
Why it Happens
Blame the scrollbar. A web page with a vertical scrollbar will need to squeeze its 100vw elements + the scrollbar gutter into a 100vw space, which can only be done by adding the horizontal scrollbar. And since scrollbar gutters are generally 12-20px wide, the horizontal scroll is tiny.
Unsurprisingly, behaviour isn't consistent across browsers or operating systems. According to CanIUse, Firefox is the only browser that correctly sets 100vw to be the window width minus the scrollbar width (if present). MacOS avoids the issue by using an overlay scrollbar that appears only when a user shows intent to scroll. But for the majority of users on Windows and Chrome, the tiny scrollbar is always an issue.
The Fix
If it's not essential for the element to span the full viewport width, it's always worth considering decreasing the element's width. Otherwise, there are plenty of solutions to this problem, so it's only a matter of picking the one that best suits your needs:
Solution 1: Hide the Overflow
Add the CSS property overflow-x: hidden
to completely disable horizontal scrolling and force the horizontal scrollbar to stay hidden. This solution is quick, and it works on all browsers, but technically, your content will be slightly off-center. Also, it doesn't work for pages that sometimes need horizontal scrolling or that have elements that don't mix well with a hidden overflow, e.g. sticky elements.
Solution 2: Use Percentages
Switch the CSS width from 100vw to 100%. Make sure that all ancestors of the element use position: static
(the default) and have their widths set to 100% as well. Like solution 1, this is a CSS-only solution and supported across all browsers. However, it won't work for elements whose parents don't have an explicit width or whose width aren't 100% of the viewport width. If setting widths for all ancestors is undesirable, don't use this solution.
Solution 3: Subtract the Scrollbar Width
Calculate the width of the scrollbar gutter with Javascript and pass it to CSS through a variable so it can be subtracted from the viewport width:
Javascript:
const body = document.querySelector("body");
const scrollbar = window.innerWidth - body.clientWidth;
body.setAttribute("style", `--scrollbar: ${scrollbar}px`);
CSS:
body {
--scrollbar: 20px; /* default if Javascript is disabled */
}
.full-width {
width: calc(100vw - var(--scrollbar));
}
This is only possible because the W3C spec requires that, for the root and body elements, clientWidth is the viewport width minus any scrollbar width. Thank you, W3C!
Note that this particular implementation uses a default scrollbar width of 20px, but it's possible to pick better defaults with media queries or user agent conditionals. That said, this isn't a great CSS-only solution. It requires a re-calculation whenever any events toggle the scrollbar visibility, e.g. when the user resizes the window or an AJAX request adds/removes elements from the page, so Javascript must be enabled for this to work responsively. Don't rely on a default.
Solution 4: Define a Custom vw
Use Javascript to calculate your own vw
unit and pass it to CSS in a variable. Though this solution's mechanics are similar to those of solution 3, I personally find it more elegant:
Javascript:
const body = document.querySelector("body");
const vw = body.clientWidth / 100;
body.setAttribute("style", `--vw: ${vw}px`);
CSS:
body {
--vw: 1vw; /* default doesn't fix the horizontal scroll */
}
.full-width {
width: calc(100 * var(--vw));
}
Similar to solution 3, event listeners are needed to keep --vw
updated whenever the scrollbar appears/disappears. Here's my extension showing how this would work for a single-file component in a Vue 2 (paired with Vue Router) application. In addition to re-calculating --vw
on window resizes, it also re-calculates on route changes:
<template>
<div id="app" :style="{ '--vw': viewportWidth }">
<!-- ...more html here... -->
</div>
</template>
<script>
export default {
name: "App",
data() {
viewportWidth: "1vw",
},
watch: {
/**
* Update the viewport width size when visiting a new page.
*/
$route() {
this.$nextTick(this.setViewportWidth);
},
},
mounted() {
this.setViewportWidth();
window.addEventListener("resize", this.setViewportWidth);
},
beforeDestroy() {
window.removeEventListener("resize", this.setViewportWidth);
},
methods: {
/*
* Set the size of 1vw so that 100vw does not include the scrollbar.
*/
setViewportWidth() {
this.viewportWidth = `${document.body.clientWidth / 100}px`;
},
},
};
</script>
<style>
#app {
--vw: 1vw; /* default */
}
.full-width {
width: calc(100 * var(--vw));
}
</style>
And that's it! Each solution comes with its own sets of pros and cons, but any of them is good enough to remove the pesky horizontal scrollbar. If any of these solutions can be improved or you know of better ones, let me know!