body-scroll-lock

Body scroll locking that just works with everything ?

Github stars Tracking Chart

Why BSL?

Enables body scroll locking (for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) without breaking scrolling of a target element (eg. modal/lightbox/flyouts/nav-menus).

Features:

  • disables body scroll WITHOUT disabling scroll of a target element
  • works on iOS mobile/tablet (!!)
  • works on Android
  • works on Safari desktop
  • works on Chrome/Firefox
  • works with vanilla JS and frameworks such as React / Angular / VueJS
  • supports nested target elements (eg. a modal that appears on top of a flyout)
  • can reserve scrollbar width
  • -webkit-overflow-scrolling: touch still works

Aren't the alternative approaches sufficient?

  • the approach document.body.ontouchmove = (e) => { e.preventDefault(); return false; }; locks the
    body scroll, but ALSO locks the scroll of a target element (eg. modal).
  • the approach overflow: hidden on the body or html elements doesn't work for all browsers
  • the position: fixed approach causes the body scroll to reset
  • some approaches break inertia/momentum/rubber-band scrolling on iOS

Package Size:

  • LIGHT - package is only 2.8KB and 1.1KB when gzipped (see here)!

Install

$ yarn add body-scroll-lock

or

$ npm install body-scroll-lock

You can also load via a <script src="lib/bodyScrollLock.js"></script> tag (refer to the lib folder).

Usage examples

Common JS
// 1. Import the functions
const bodyScrollLock = require('body-scroll-lock');
const disableBodyScroll = bodyScrollLock.disableBodyScroll;
const enableBodyScroll = bodyScrollLock.enableBodyScroll;
  
// 2. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
const targetElement = document.querySelector("#someElementId");
  
  
// 3. ...in some event handler after showing the target element...disable body scroll
disableBodyScroll(targetElement);
 
 
// 4. ...in some event handler after hiding the target element...
enableBodyScroll(targetElement);
React/ES6
// 1. Import the functions
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
  
class SomeComponent extends React.Component {
  targetElement = null;
  
  componentDidMount() {
    // 2. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 
    // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
    // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
    this.targetElement = document.querySelector('#targetElementId');
  }
  
  showTargetElement = () => {
    // ... some logic to show target element
    
    // 3. Disable body scroll
    disableBodyScroll(this.targetElement);
  };
  
  hideTargetElement = () => {
    // ... some logic to hide target element
    
    // 4. Re-enable body scroll
    enableBodyScroll(this.targetElement);
  }
  
  componentWillUnmount() {
    // 5. Useful if we have called disableBodyScroll for multiple target elements,
    // and we just want a kill-switch to undo all that.
    // OR useful for if the `hideTargetElement()` function got circumvented eg. visitor 
    // clicks a link which takes him/her to a different page within the app.
    clearAllBodyScrollLocks();
  }

  render() {   
    return (
      <div>
        some JSX to go here
      </div> 
    );
  }
}
React/ES6 with Refs
// 1. Import the functions
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
  
class SomeComponent extends React.Component {
  // 2. Initialise your ref and targetElement here
  targetRef = React.createRef();
  targetElement = null;

  componentDidMount() {
    // 3. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav). 
    // Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
    // This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
    this.targetElement = this.targetRef.current; 
  }
  
  showTargetElement = () => {
    // ... some logic to show target element
    
    // 4. Disable body scroll
    disableBodyScroll(this.targetElement);
  };
  
  hideTargetElement = () => {
    // ... some logic to hide target element
    
    // 5. Re-enable body scroll
    enableBodyScroll(this.targetElement);
  }
  
  componentWillUnmount() {
    // 5. Useful if we have called disableBodyScroll for multiple target elements,
    // and we just want a kill-switch to undo all that.
    // OR useful for if the `hideTargetElement()` function got circumvented eg. visitor 
    // clicks a link which takes him/her to a different page within the app.
    clearAllBodyScrollLocks();
  }

  render() {   
    return (
      // 6. Pass your ref with the reference to the targetElement to SomeOtherComponent
      <SomeOtherComponent ref={this.targetRef}>
        some JSX to go here
      </SomeOtherComponent> 
    );
  }
}

// 7. SomeOtherComponent needs to be a Class component to receive the ref (unless Hooks - https://reactjs.org/docs/hooks-faq.html#can-i-make-a-ref-to-a-function-component - are used).
class SomeOtherComponent extends React.Component {

  componentDidMount() {
    // Your logic on mount goes here
  }

  // 8. BSL will be applied to div below in SomeOtherComponent and persist scrolling for the container
  render() {   
    return (
      <div>
        some JSX to go here
      </div> 
    );
  }
}
Vanilla JS

In the html:

<head>
  <script src="some-path-where-you-dump-the-javascript-libraries/lib/bodyScrollLock.js"></script>
</head>

Then in the javascript:

// 1. Get a target element that you want to persist scrolling for (such as a modal/lightbox/flyout/nav).
// Specifically, the target element is the one we would like to allow scroll on (NOT a parent of that element).
// This is also the element to apply the CSS '-webkit-overflow-scrolling: touch;' if desired.
const targetElement = document.querySelector("#someElementId");

// 2. ...in some event handler after showing the target element...disable body scroll
bodyScrollLock.disableBodyScroll(targetElement);

// 3. ...in some event handler after hiding the target element...
bodyScrollLock.enableBodyScroll(targetElement);

// 4. Useful if we have called disableBodyScroll for multiple target elements,
// and we just want a kill-switch to undo all that.
bodyScrollLock.clearAllBodyScrollLocks();

Demo

Check out the demo, powered by Now, @ https://bodyscrolllock.now.sh

Caveat

On iOS mobile (as is visible in the above demo), if you scroll the body directly even when the scrolling is
locked (on iOS), the body scrolls - this is not what this package solves. It solves the typical case where a modal
overlays the screen, and scrolling within the modal never causes the body to scroll too (when the top or bottom
within the modal has been reached).

Since the update from @Neddz, this caveat is no longer valid. iOS mobile behaviour should be the same as
other devices (eg. Android Chrome).

Functions, Function, Arguments, Return, Description, :---, :---, :---:, :---, disableBodyScroll, targetElement: HTMLElement options: BodyScrollOptions, void, Disables body scroll while enabling scroll on target element, enableBodyScroll, targetElement: HTMLElement, void, Enables body scroll and removing listeners on target element, clearAllBodyScrollLocks, null, void, Clears all scroll locks, ## Options

reserveScrollBarGap

optional, default: false

If the overflow property of the body is set to hidden, the body widens by the width of the scrollbar. This produces an
unpleasant flickering effect, especially on websites with centered content. If the reserveScrollBarGap option is set,
this gap is filled by a padding-right on the body element. If disableBodyScroll is called for the last target element,
or clearAllBodyScrollLocks is called, the padding-right is automatically reset to the previous value.

import { disableBodyScroll } from 'body-scroll-lock';
import type { BodyScrollOptions } from 'body-scroll-lock';

const options: BodyScrollOptions = {
    reserveScrollBarGap: true
}

disableBodyScroll(targetElement, options);

allowTouchMove

optional, default: undefined

There are cases where you have called disableBodyScroll on an element, but you still want some or all
children of it to receive touch moves still; or in other words, you want child elements to
ignore the fact that a parent element has the body scroll lock set (and hence, not be affected at all by this setting).
See below for 2 use cases:

Simple
  disableBodyScroll(container, {
    allowTouchMove: el => (el.tagName === 'TEXTAREA')
  });
More Complex

Javascript:

  disableBodyScroll(container, {
    allowTouchMove: el => {
      while (el && el !== document.body) {
        if (el.getAttribute('body-scroll-lock-ignore') !== null) {
          return true
        }
  
        el = el.parentNode
      }
    },
  });

Html:

  <div id="container">
    <div id="scrolling-map" body-scroll-lock-ignore>
      ...
    </div>
  </div>

References

https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177
https://stackoverflow.com/questions/41594997/ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-posi

Changelog

Refer to the releases page.

Main metrics

Overview
Name With Ownerwillmcpo/body-scroll-lock
Primary LanguageJavaScript
Program languageJavaScript (Language Count: 1)
Platform
License:MIT License
所有者活动
Created At2018-01-19 05:09:01
Pushed At2023-03-30 18:49:50
Last Commit At2021-08-14 11:16:19
Release Count62
Last Release Namev4.0.0-beta.0 (Posted on 2021-06-23 23:12:57)
First Release Namev1.0.1 (Posted on 2018-01-19 17:06:15)
用户参与
Stargazers Count4.1k
Watchers Count29
Fork Count332
Commits Count293
Has Issues Enabled
Issues Count164
Issue Open Count83
Pull Requests Count58
Pull Requests Open Count37
Pull Requests Close Count14
项目设置
Has Wiki Enabled
Is Archived
Is Fork
Is Locked
Is Mirror
Is Private