Scroll Locking

Prevent your user from scrolling behind the scenes.

CSS
,

When would I need scroll locking

On the web, when you create an overlay/modal/dialog you usually begin by using the CSS:

position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;

This is great, you can give this new <div> a transparent background and it's children a solid background and then you have a modal. It is at this point that you realise the page you have covered up can still be scrolled up and down.

This may not seem like much of an issue, but it provides a very strange experience for the user should they inadvertently scroll down the page without knowing before closing the dialog. Even worse if you allow this to happen on a dialog.

Solution

The solution to this problem is a simple one, but not an obvious one. You would be forgiven for expecting there to be a CSS style to prevent scrolling on the <body>however, the only solution is to turn to JavaScript.

import React, { Component } from 'react'

class ScrollStopper extends Component {

 componentDidMount() {
   const offsetFromTop = window.scrollY;
   document.body.style.position = 'fixed'
   document.body.style.top = `-${offset}px`
 }
  
  componentWillUnmount() {
    const scrollY = document.body.style.top
    document.body.style.position = ''
    document.body.style.top = ''
    window.scrollTo(0, parseInt(scrollY || '0') * - 1)
  }
  
  render() {
    return null
  }
}

export default ScrollStopper

There is a fair amount to unpack here. First of all while we all enjoy React hooks, I find for a component with actions that are only lifecycle based a class based approach is less likely to be mistaken.

First of all this component needs to be contained within the dialog or modal component so that it mounts and unmounts with the dialog. This is safer than adding classes to the body as a side effect, for example as part of a redux thunk because it all the logic is contained within the <ScrollLock /> component and that component is contained within the <Dialog />. This prevents any possibility of locking your user to the top of the screen accidentally.

componentDidMount()

Lets run through what this function is doing and why.

  1. Record the offset from the top before doing anything else. We need to record this before we prevent scrolling to maintain the page in it's current location.
  2. Set the <body> to have a position of fixed. This prevent scrolling on any device screen size.
  3. Quickly offset the body from the top by the same distance we had already scrolled down

The only noticeable change after this completes is that the scroll bar will disappear when the dialog opens. If this bothers you, you can always add padding to match the width of the scroll bar.

componentWillUnmount()

Now we run these changes back in reverse and your user will be none the wiser.

  1. Re-use the top value we updated when mounting the component
  2. Reset the values for position and fixed
  3. Scroll instantly to the value recorded in the top offset, remembering that styles are stored as strings and reversing the negative value by multiplying by -1.

render()

No need to render anything, this component is all about the logic it contains, using React's lifecycle hooks to fire off the functions for us.

Conclusion

This is a useful component to have in your library as it will save you from embarrassing questions when users scroll the page behind your transparent overlay or see two scroll bars on a full screen dialog/modal.

Troubleshooting

Depending on how you have set up your <body>, if you have used flex-grow to expand your page to fill the screen, setting the screen to be fixed can cause your content to jump left and only take up the space it requires. In this case add `width: 100%` when the component mounts and remove it during componentWillUnmount in addition to the other CSS styles. This will ensure the body fills the screen space.

© ARCHILTON LTD 2023