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

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

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

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

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

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.

@solutweb

This comment has been minimized.

Copy link

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

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

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

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

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

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

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.