I came across a frustrating CSS issue while working on my personal website.

I had a section which needed to stretch across the full width of the browser window. Normally, you’d expect width: 100vw to do exactly that.

It worked perfectly on some devices, but on others, the element was slightly wider than the page and caused an unwanted horizontal scrollbar.

After a bit of digging, I discovered that the problem was the browser’s vertical scrollbar.

Why 100vw isn’t always 100%

The issue comes from how CSS viewport units are calculated.

100vw represents the width of the viewport, but on browsers with a traditional scrollbar, that measurement can include the space occupied by the vertical scrollbar.

For example, if the viewport is 1440 pixels wide and the vertical scrollbar takes up 15 pixels, the available content width is only 1425 pixels.

An element with width: 100vw can still be 1440 pixels wide, making it 15 pixels too large.

The annoying part is that it doesn’t happen everywhere.

Some operating systems use overlay scrollbars, which sit on top of the content rather than taking up space. Others reserve a permanent area for the scrollbar.

This means a layout can look perfectly fine on one computer and have horizontal overflow on another.

Calculating the scrollbar width

I found an article by Destroy Todaydestroytoday.com favicon which explained the problem and gave me the idea for a solution.

JavaScript provides two measurements which can be used to work out how much space the scrollbar occupies.

window.innerWidth gives us the viewport width, including the scrollbar.

document.documentElement.clientWidth gives us the width available to the document, excluding the traditional scrollbar.

Subtracting one from the other gives us the width of the scrollbar.

1const scrollbarWidth =
2 window.innerWidth - document.documentElement.clientWidth
Copied!

On a device with overlay scrollbars, this will typically return 0.

On a device with a traditional scrollbar, it might return something like 15.

Rather than hardcoding a value, I can use the actual width reported by the browser.

Making the value available to CSS

I didn’t want to calculate element widths in JavaScript. CSS is perfectly capable of handling the layout once it knows how wide the scrollbar is.

I created a small JavaScript class which calculates the scrollbar width and stores it in a CSS custom property.

1class ScrollbarWidth {
2 constructor () {
3 this.width = window.innerWidth - document.documentElement.clientWidth
4 this.oldWidth = null
5 this.ticking = false
6
7 this.update()
8
9 window.addEventListener('resize', () => {
10 this.width = window.innerWidth - document.documentElement.clientWidth
11
12 if (this.oldWidth === this.width) {
13 return
14 }
15
16 this.requestTick()
17 })
18 }
19
20 requestTick () {
21 if (this.ticking) {
22 return
23 }
24
25 requestAnimationFrame(() => {
26 this.update()
27 })
28
29 this.ticking = true
30 }
31
32 update () {
33 document.documentElement.style.setProperty('--scrollbar-width', `${this.width}px`)
34
35 this.oldWidth = this.width
36 this.ticking = false
37 }
38}
39
40new ScrollbarWidth()
Copied!

This makes the scrollbar width available throughout the stylesheet as var(--scrollbar-width).

I also added a resize listener because the value can change when the browser window is resized or its scrollbar behaviour changes.

We use requestAnimationFrame() to schedule updates, avoiding unnecessary repeated changes to the DOM.

Creating a corrected viewport width

Now that CSS knows how wide the scrollbar is, I can subtract it from 100vw.

I added another custom property to my global stylesheet.

1:root {
2 --100vw: calc(100vw - var(--scrollbar-width, 0px));
3}
Copied!

The fallback of 0px means that the calculation will still work before JavaScript has run.

Instead of using 100vw directly, I can now use var(--100vw) wherever I need the width of the visible page.

On devices without a traditional scrollbar, the calculation is effectively unchanged.

On devices where the scrollbar takes up space, that width is subtracted.

Using it on my website

One place where I needed this was the horizontally scrolling sections on my homepage.

On smaller screens, the cards change from a normal grid into a horizontal carousel. The carousel needs to extend to the edges of the viewport, even though it’s inside a centred container.

I’m using CSS Grid, with calculated column widths and negative margins to achieve this.

1@media (max-width: 768px) {
2 .grid {
3 grid-auto-flow: column;
4 grid-template-columns:
5 30px
6 repeat(7, calc(var(--100vw) - 80px))
7 30px;
8 
9 max-width: var(--100vw);
10 
11 margin-right:
12 calc(-1 * ((var(--100vw) - 100%) / 2));
13 
14 margin-left:
15 calc(-1 * ((var(--100vw) - 100%) / 2));
16 
17 overflow-x: auto;
18 scroll-snap-type: x mandatory;
19 }
20}
Copied!

The important part is that all of the calculations use the corrected viewport width.

The negative margins allow the carousel to escape its parent container and stretch to the edges of the page, without accidentally extending underneath the vertical scrollbar.

The cards can then scroll horizontally using CSS scroll snapping.

Without correcting the viewport width, the alignment could be slightly off on devices with traditional scrollbars.

A small fix for an annoying problem

It’s one of those CSS issues which is easy to miss when developing on a single machine.

Everything can look fine until someone opens the website on a different operating system or changes their scrollbar settings.

The solution ended up being a small JavaScript file and a CSS variable.

There are other approaches, such as using percentage widths where possible or avoiding viewport units entirely. Modern CSS also provides some additional viewport units, although they don’t directly solve this traditional scrollbar issue.

For my layout, I still wanted to use viewport-based calculations, so measuring the scrollbar was a practical solution.

Now I can use var(--100vw) anywhere I need the full available viewport width, without worrying about whether the device has an overlay scrollbar or a traditional one.