loke.dev
Header image for The Reserved Seat for the Scrollbar

The Reserved Seat for the Scrollbar

Why your 'perfect' modal transitions still trigger layout shifts on Windows and Linux, and how the scrollbar-gutter property provides a native fix.

· 4 min read

It’s a classic move: you spend three hours perfecting a modal’s entry animation on your MacBook, only to open the staging link on a Windows machine and watch the entire layout twitch like it’s had too much espresso.

The culprit is almost always the scrollbar. Or, more accurately, the sudden absence of one.

When you trigger a modal, you likely (and correctly) set overflow: hidden on the <body> to prevent the user from scrolling the background content into oblivion. On macOS, where scrollbars are often thin, floating overlays, this doesn't change much. But on Windows, Linux, or anyone using a physical mouse with "always show scrollbars" toggled on, that scrollbar is a physical entity. It occupies about 15 to 20 pixels of horizontal real estate.

When it vanishes, the viewport width expands, and your beautifully centered content "jumps" to the right to fill the gap. It's a janky experience that makes a pro site feel like a weekend project.

The Old Way: Math and Regret

For years, we solved this with "magic padding." We’d use a snippet of JavaScript to calculate the width of the system's scrollbar, then manually inject that value as padding-right on the body whenever a modal opened.

It looked something like this:

// Don't do this anymore if you can help it
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.paddingRight = `${scrollbarWidth}px`;
document.body.style.overflow = 'hidden';

It worked, but it was brittle. It felt like we were fighting the browser rather than working with it. If the window resized or the zoom level changed, the math could get weird.

Enter scrollbar-gutter

CSS finally gave us a native way to say, "Hey, save a seat for the scrollbar, even if it isn't here right now."

The scrollbar-gutter property allows you to reserve that space so that the appearance or disappearance of a scrollbar doesn't change the size of the content box.

The implementation is incredibly simple:

html {
  scrollbar-gutter: stable;
}

By adding stable to your html or body tag, the browser will always reserve that 15-20px strip on the side. When your modal opens and you apply overflow: hidden, the scrollbar disappears, but the gutter stays. Your layout stays perfectly still. No jump, no twitch, no JavaScript required.

Centering and the "Both Edges" Problem

If you are a perfectionist (and if you're reading a post about 15-pixel layout shifts, you probably are), you’ll notice a new problem. If you have a perfectly centered container, reserving space for a scrollbar on the right side technically pushes your "center" a few pixels to the left.

To the average user, it's invisible. To you, it's an itch you can't scratch.

The spec accounts for this with the both-edges keyword:

html {
  scrollbar-gutter: stable both-edges;
}

This reserves space on both the left and right sides. This ensures that your content remains perfectly symmetrical within the viewport, regardless of whether a scrollbar is present.

A Practical Implementation

Here is how I usually structure this in a modern project. I apply it to the html element to ensure the entire document respects the reserved space.

:root {
  /* Reserve space to prevent layout shift on Windows/Linux */
  scrollbar-gutter: stable;
}

body.modal-open {
  /* When the modal is active, prevent background scrolling */
  overflow: hidden;
}

One thing to keep in mind: scrollbar-gutter only has an effect if the element isn't an "overlay" scrollbar. If the user is on a modern Mac or a mobile device where scrollbars don't take up space anyway, this property effectively does nothing. It’s the ultimate progressive enhancement.

Where can you use it?

Support is actually quite good. It’s been in Chromium (Chrome, Edge, Brave) since version 94 and Firefox since version 97.

Safari is, as of this writing, the lone holdout. However, since most macOS users have overlay scrollbars by default, the "jump" isn't a problem for them anyway. You’re essentially fixing the bug for the people who actually see it (Windows/Linux users) and leaving the experience unchanged for those who don't.

The Verdict

Layout shifts are one of those "small" things that contribute heavily to the perceived quality of a web application. We talk a lot about Core Web Vitals and Cumulative Layout Shift (CLS) in the context of images and ads, but our own UI interactions shouldn't be the cause of instability.

If you’re still using JavaScript to calculate scrollbar offsets, give scrollbar-gutter: stable a try. It’s one of those rare CSS properties that solves a decade-old headache with a single line of code.