Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
import { useState, useLayoutEffect } from 'react';
// Usage
function App(){
// State for our modal
const [modalOpen, setModalOpen] = useState(false);
return (
<div>
<button onClick={() => setModalOpen(true)}>Show Modal</button>
<Content />
{modalOpen && (
<Modal
title="Try scrolling"
content="I bet you you can't! Muahahaha 😈"
onClose={() => setModalOpen(false)}
/>
)}
</div>
);
}
function Modal({ title, content, onClose }){
// Call hook to lock body scroll
useLockBodyScroll();
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal">
<h2>{title}</h2>
<p>{content}</p>
</div>
</div>
);
}
// Hook
function useLockBodyScroll() {
useLayoutEffect(() => {
// Get original value of body overflow
const originalStyle = window.getComputedStyle(document.body).overflow;
// Prevent scrolling on mount
document.body.style.overflow = 'hidden';
// Re-enable scrolling when component unmounts
return () => document.body.style.overflow = originalStyle;
}, []); // Empty array ensures effect is only run on mount and unmount
}
@sstur

This comment has been minimized.

Copy link

sstur commented Jan 18, 2019

It's worth noting that browsers / operating systems that use a non-hiding scrollbar (I think most versions of Windows) will show/hide the scrollbar when you change the body overflow from hidden to visible.

It's a tricky one to work around, but in the past I had some luck with first measuring the width/height of the body and then fixing that size before changing the overflow. That prevents the change of layout when you enable/disable scrolling.

@anyexinglu

This comment has been minimized.

Copy link

anyexinglu commented Jan 19, 2019

It's worth noting that browsers / operating systems that use a non-hiding scrollbar (I think most versions of Windows) will show/hide the scrollbar when you change the body overflow from hidden to visible.

It's a tricky one to work around, but in the past I had some luck with first measuring the width/height of the body and then fixing that size before changing the overflow. That prevents the change of layout when you enable/disable scrolling.

I agree, setting document.body.style.paddingRight may resolve the problem.

@anyexinglu

This comment has been minimized.

Copy link

anyexinglu commented Jan 19, 2019

useLayoutEffect(() => {
    // Prevent scrolling on mount
    document.body.style.overflow = 'hidden';
    // Re-enable scrolling when component unmounts
    return () => (document.body.style.overflow = 'visible');
  }, []); // Empty array ensures effect is only run on mount and unmount

return () => (document.body.style.overflow = 'visible')
It's better to reset document.body.style.overflow into original value, such as auto/ hidden (sometimes isn't visible)

Writing these with hooks is great, which decreases the code lines.

@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Jan 19, 2019

@sstur Good point! If the body has a background color then they may still have a white area where the scrollbar was.. but I supposed that is better than actual body content moving.

@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Jan 19, 2019

@anyexinglu Happy to accept a pull request (we're now open source at https://github.com/gragland/usehooks) or look at a code example in this gist. I supposed we'd just have to grab the current document.body.style and save to state or ref, and then use that when effect cleans up.

@brunodesde1987

This comment has been minimized.

Copy link

brunodesde1987 commented Feb 18, 2019

EDIT: First of all, thanks for this. 👍

Saddly doesn't work on mobile 😞.

I tried on my Moto G5s + Chrome and on iPhone 8 Plus + Chrome too 🤔:
uselockbodyscroll

@gragland

This comment has been minimized.

Copy link
Owner Author

gragland commented Feb 19, 2019

@solutweb Ah good point. When I have some time will have to dig into what's going on there. Open to pull requests if you happen to figure it out!

@heyflynn

This comment has been minimized.

Copy link

heyflynn commented Aug 15, 2019

in addition to width issue @anyexinglu pointed out about windows scrollbars, there is also an issue with with fixed backgrounds resizing when you overflow:hidden the scrollbars. material-ui tries to resolve this by adding padding to any element with the class mui-fixed.

@jescowuester

This comment has been minimized.

Copy link

jescowuester commented Aug 20, 2019

Why does it use useLayoutEffect? Seems unnecessary to me since we only use it on mount and unmount.

@lcvbeek

This comment has been minimized.

Copy link

lcvbeek commented Sep 25, 2019

When using it in TypeScript, it complains that the first param doesn't match EffectCallback, since it's returning () => string.

Changing return () => (document.body.style.overflow = 'visible') into return () => {document.body.style.overflow = 'visible'} fixes this.

@JohnBRTS

This comment has been minimized.

Copy link

JohnBRTS commented Oct 8, 2019

This currently won't work for iOS devices. You could rewrite it like this

  // Create ref to bind to element that should prevent scrolling
  const ref = useRef(null);
  useEffect(() => {
    // Add/Remove listeners for touchmove -> finding touchmove better, as it doesn't prevent other touch events like clicking links
    // The checks for ref && ref.current made it more TS friendly for me
    if (ref && ref.current) {
      ref.current.addEventListener('touchmove', e => e.preventDefault());
    }
    return () => {
      if (ref && ref.current) {
        ref.current.removeEventListener('touchmove', e => e.preventDefault());
      }
    };
  }, []);
  useLayoutEffect(() => {
    // Get original body overflow
    const originalStyle = window.getComputedStyle(document.body).overflow;
    // Prevent scrolling on mount
    document.body.style.overflow = 'hidden';
    // Re-enable scrolling when component unmounts
    return () => {
      return (document.body.style.overflow = originalStyle);
    };
  }, []); // Empty array ensures effect is only run on mount and unmount
  // Return the ref, to bind it to the element;
  return ref;
}````

And by binding the ref to the element that locks the scrolling, iOS devices will also respect the scroll lock
@dallanlee

This comment has been minimized.

Copy link

dallanlee commented Oct 11, 2019

I took a shot at modifying this to make it work:

  1. On iOS
  2. When multiple "instances" of lockBodyScroll (modals in this case) are active

It seems to be working with my changes in my sandbox fork here: https://codesandbox.io/s/uselockbodyscroll-example-mq9rm.
Or test it on your iOS device here: https://mq9rm.csb.app/

But I'm pretty new to custom hooks (and programming, really) so I'm open to suggestions/critique 👍🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.