-
-
Save gragland/f50690d2724aec1bd513de8596dcd9b9 to your computer and use it in GitHub Desktop.
| 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 | |
| } |
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.
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 unmountreturn () => (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.
@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.
@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 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!
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.
Why does it use useLayoutEffect? Seems unnecessary to me since we only use it on mount and unmount.
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.
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
I took a shot at modifying this to make it work:
- On iOS
- 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 ππΌ

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.