No Scroll Body Hook

Boston CartwrightBoston Cartwright | December 2, 2020 | react
6 min read | ––– views
Photo by Eco Warrior Princess on Unsplash
Photo by Eco Warrior Princess on Unsplash

Preventing the background contents from scrolling while having a modal open

Want to check this out later?

Co-Authored by Adam Whitehurst and Boston Cartwright

This last week Adam and I were working on a task to fix a peculiar bug. ​ We had a page with a large, scrolling table, and toggleable modal. This modal had some content in it that you could also scroll through.

The bug was that onceyou got to the end of the modal content, scrolling would scroll the table content! 😲

Below is an embed of a codesandbox showing the issue. If you open the modal and attempt to scroll to the end of the modal content, and then scroll again it will scroll the background! ​


After some researching, we determined the best solution was to set a css property, overflow: hidden, which prevents scrolling, on the container of the background content.

This works to stop scrolling, but we only want scrolling to stop when the modal is open, so we needed to only apply that styling sometimes. ​

Our naive approach was to do this by holding a state in the parent whether the modal was open, and if that state is there to add the css attribute. Something like this: ​

function Parent() {
  const [isOpen, setIsOpen] = React.useState(false);return (
    <div className={`${isOpen ? 'no-scroll-class' : ''}`}>
    {isOpen ? <Modal /> : null}
    {/* ... lots of other content */}
    </div>
  )
}

This worked, but it's more than the parent signed up for when it just wants to render a Modal component. We could hear the other developers saying: "So now every time we want a Modal we also have to add this janky state and css stuff?"

Since we are all lazy developers, we would likely just 'forget' until the bug tickets came in. 😒 We felt that there had to be a more Reactive 🚀 way to do it.


Because React problems require React solutions, we turned to hooks, of course (although another custom component would have worked just as well). It ended up looking like this: ​

React.useEffect(() => {
  const className = `body-no-scroll`
  if (!document.body.classList.contains(className)) {
    document.body.classList.add(className)
  }
  return () => document.body.classList.remove(className)
}, [])

​ This worked really well, and solved our issue. This could be used inside of our Modal component, and the parent did not have to worry about the issue at all. No other developers would have to get their hands dirty with css and so everyone was happy. 😊 ​


Once we developed this, seeing it was just a small side effect, we discovered we could even create a custom hook for it, looking like this (with the body-no-scroll class containing overflow: hidden):

function useNoScrollBody() {
  React.useEffect(() => {
    const className = `body-no-scroll`
    if (!document.body.classList.contains(className)) {
      document.body.classList.add(className)
    }
    return () => document.body.classList.remove(className)
  }, [])
}

​ This made a generic hook that could then be used for any other modals (or other component) in our application, to ensure that the background content does not scroll as well. ​ Here is what it looked like in the end: ​


Extra Generification

​ Once this solution was done, it was obvious we could go overboard with the generification of this code by abstracting out element and className we wanted this hook to act on. If we were so inclined, it would be as simple as writing the function signature like so:

function useClassOnMount(
className,
element
) {
  React.useEffect(() => {
    if (!element.classList.contains(className)) {
      element.classList.add(className);
    }
    return () => element.classList.remove(className);
  }, [])
}function useNoScrollBody() {
    return useClassOnMount('no-scroll', document.body);
}

But, of course, you should always keep things as simple as possible so we won't be generifying our hook until the need arises, but that may be written as another post. 🙂 Until then! ​

What do you think? Tweet us @bstncartwright & @brighthurst