Scrollbar behavior

I spent hours perfecting a modal, then watched my entire page shift 12 to 16 pixels to the right when I opened it. The culprit? The scrollbar.

When you add overflow: hidden to prevent background scrolling, the scrollbar disappears. The content expands to fill that space, everything shifts right, then shifts back when you close the modal.

This is one of those details that separates "it works" from "it feels good." And it drove me nuts.

Loading demo...

What I learned about scrollbars

Scrollbars take up physical space about 15-17 pixels depending on your browser and OS. When you hide overflow, you're not just hiding the scrollbar, you're giving the content that space back. The page doesn't know you're coming back for it.

I noticed the problem most when:

  • Opening modals (the classic case)
  • My fixed navigation shifting independently from the page
  • Switching between pages of different lengths
  • Any time content height changed dynamically

The worst part? It looked almost perfect. Just janky enough to notice.

First, I made it pretty

Before fixing the shift, I wanted my scrollbar to actually match my design. Default browser scrollbars stick out like a sore thumb in dark mode interfaces.

The styling part is actually straightforward. Here's what I use across my site:

/* Firefox */
html,
body,
* {
  scrollbar-width: thin;
  scrollbar-color: var(--scrollbar-thumb) transparent;
}

/* Webkit browsers (Chrome, Safari, Edge) */
html::-webkit-scrollbar,
body::-webkit-scrollbar,
*::-webkit-scrollbar {
  width: 12px;
}

html::-webkit-scrollbar-track,
body::-webkit-scrollbar-track,
*::-webkit-scrollbar-track {
  background: transparent;
  box-shadow: inset 1px 0 0 0 var(--section-stroke);
}

html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb,
*::-webkit-scrollbar-thumb {
  background: var(--scrollbar-thumb);
  border-radius: 9999px;

  /* Border trick for padding */
  border: 3.5px solid transparent;
  border-left: 4px solid transparent;
  background-clip: content-box;
}

A few things happening here: Firefox gets a thin scrollbar, Webkit browsers get a fully custom one with a subtle track border (via box-shadow), and the border trick on the thumb creates visual padding without affecting its width. Nice and clean. But it still shifts my layout when it disappears.

The real problem: the shift

The typical modal code looks like this:

// Open modal
document.body.style.overflow = 'hidden';

// Close modal
document.body.style.overflow = '';

When overflow: hidden kicks in, the scrollbar vanishes. The content goes "sweet, extra pixels!" and shifts right. Close the modal, scrollbar comes back, content shifts left again.

I noticed it most on my navigation bar it would jump left and right like it was dancing. Not the vibe I was going for.

The shift is especially obvious when you have:

  • A fixed header (it moves independently from the page)
  • Centered content (the center point literally changes)
  • Text the user is reading (their eyes notice immediately)

How I fixed it

The trick is to replace the scrollbar's space with padding before hiding it. If the scrollbar is 17px wide, add 17px of padding-right to the body. When the scrollbar disappears, the padding keeps everything in place.

The CSS-only way

There's also scrollbar-gutter: stable which reserves space for the scrollbar even when it's not visible. Clean solution, but browser support is still catching up:

html {
  scrollbar-gutter: stable;
}

Works in modern Chrome and Firefox. I use the JavaScript approach for broader compatibility.

Expanding the logic

Here's what worked for me:

function getScrollbarWidth() {
  return window.innerWidth - document.documentElement.clientWidth;
}

function openModal() {
  const scrollbarWidth = getScrollbarWidth();

  document.body.style.paddingRight = `${scrollbarWidth}px`;
  document.body.style.overflow = 'hidden';

  // Don't forget fixed elements!
  const header = document.querySelector('.fixed-header');
  if (header) {
    header.style.paddingRight = `${scrollbarWidth}px`;
  }
}

function closeModal() {
  document.body.style.paddingRight = '';
  document.body.style.overflow = '';

  const header = document.querySelector('.fixed-header');
  if (header) {
    header.style.paddingRight = '';
  }
}

The key insight: measure the scrollbar width dynamically. Different browsers and operating systems have different scrollbar widths. Don't hardcode it.

Make it reusable

Instead of copying this code everywhere, wrap it in a reusable hook/composable. Here's what I use:

Loading framework examples...

Bonus: The composable also sets --scrollbar-offset as a CSS variable. Use it on fixed elements to prevent them from shifting:

.fixed-header {
  position: fixed;
  right: 40px;
  /* Compensate for scrollbar */
  transform: translateX(calc(var(--scrollbar-offset, 0px) * 0.5));
}

Things I wish I knew earlier

Mobile doesn't have this problem. Overlay scrollbars don't take up space. This is a desktop-only issue.

Measure dynamically. Chrome uses 17px, Safari 15px, Firefox does its own thing. Never hardcode the width.

Use the CSS variable. Setting --scrollbar-offset lets you compensate any fixed element without hunting down hardcoded values.

Make it reusable immediately. Don't copy-paste this three times like I did. One hook, use everywhere.

Wrapping up

Most users won't consciously notice this fix—but they'll feel it. Smooth modals feel polished. Janky ones feel cheap.

The solution is simple: measure scrollbar width, add padding, hide overflow. Two lines of code, zero dependencies.

Hope this helps with your modals! If you found this useful, share it with someone who might need it.