Max Schmitt

February 15 2022

React: Preventing Layout Shifts When Body Becomes Scrollable

When content on a website changes its length, it can cause the layout to shift to the side if a user's browser always shows scrollbars:

This is because the scrollbar takes up a certain amount of space which is missing when the scrollbar is not visible.

The Simple Solution: Always Show Scrollbars

The easiest way to avoid the layout shift from happening, is to always display scrollbars using CSS:

CSS

html {
overflow: scroll;
}

This will make sure that the scrollbar is always visible, even if the content isn't scrollable.

The Complex Solution: Fake Scrollbars When They Aren't There

If you don't want to show a scrollbar when the content isn't scrollable, you can still prevent the layout shift by making room for the scrollbar when it isn't visible.

Here are the steps we want to take:

  1. Determine the width of the scrollbar
  2. Find out when body becomes scrollable
  3. Apply extra padding when scrollbar is not visible

Note

In the future we'll be able to use the scrollbar-gutter CSS property. At the time of writing this post, browser compatibility isn't quite there yet, but we should get there soon.

1. Getting the Width of the Scrollbar

There is a really great answer on StackOverflow for a function getScrollbarWidth() which is exactly what we need.

Let's copy it and add it to our codebase.

2. Finding Out When the Body Becomes Scrollable

To find out when the body becomes scrollable, we can make use a ResizeObserver on document.body. Let's package this up in a custom React hook called useBodyScrollable():

JS

function useBodyScrollable() {
const [bodyScrollable, setBodyScrollable] = useState(document.body.scrollHeight > window.innerHeight)
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
setBodyScrollable(document.body.scrollHeight > window.innerHeight)
})
resizeObserver.observe(document.body)
return () => {
resizeObserver.unobserve(document.body)
}
}, [])
return bodyScrollable
}

3. Applying Extra Padding When Scrollbar Is Not Visible

Combining our getScrollbarWidth() function and our useBodyScrollable() hook, we can now adjust the body's padding in a getLayoutEffect():

JS

const scrollbarWidth = getScrollbarWidth()
function MyLayoutComponent() {
const bodyScrollable = useBodyScrollable()
useLayoutEffect(() => {
if (bodyScrollable) {
document.body.style.paddingRight = '0px'
} else {
document.body.style.paddingRight = `${scrollbarWidth}px`
}
}, [bodyScrollable])
return // ...
}

The Result

With these pieces place, our body now properly adjust itself depending on the scrollbar's width and whether or not it's currently visible:

You can view the entire code on CodeSandbox