body-scroll-lock vs scroll-lock
Scroll Locking Libraries for Web Overlays
body-scroll-lockscroll-lockSimilar Packages:
Scroll Locking Libraries for Web Overlays

body-scroll-lock and scroll-lock are JavaScript libraries designed to prevent the main document body from scrolling while allowing specific elements (like modals or side panels) to remain scrollable. This is commonly needed in UIs with overlays to avoid background scroll interference and maintain focus on the active layer. body-scroll-lock is a modern, actively maintained solution that handles cross-browser quirks, mobile touch behavior, and nested scroll contexts. scroll-lock is a simpler, older alternative that has been officially deprecated and should not be used in new projects.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
body-scroll-lock1,227,0594,106-1205 years agoMIT
scroll-lock33,611273-45 years agoMIT

Controlling Body Scroll: body-scroll-lock vs scroll-lock

When building modal dialogs, side panels, or other overlays that should lock the background from scrolling, developers often reach for a utility library. Both body-scroll-lock and scroll-lock aim to solve this problem, but they differ significantly in implementation, API design, and current maintenance status. Let’s break down what each offers and how they behave in real-world scenarios.

🚫 Current Maintenance Status

Before diving into features, it’s critical to note the maintenance status of these packages.

  • scroll-lock is deprecated. According to its npm page, it explicitly states: "This package has been deprecated. Please use body-scroll-lock instead." The GitHub repository is archived, and no updates have been published in years.
  • body-scroll-lock remains actively maintained, with recent releases addressing browser compatibility and edge cases.

⚠️ Do not use scroll-lock in new projects. It is officially deprecated and unsupported.

🔒 Core Functionality: What These Libraries Actually Do

Both libraries attempt to prevent the <body> from scrolling while allowing internal scrollable regions (like a modal with overflow content) to remain scrollable. They do this by:

  1. Disabling overflow on the body
  2. Compensating for the loss of the scrollbar’s width to prevent layout shift
  3. Optionally preserving scroll position
  4. Re-enabling scroll when unlocked

However, their approaches and reliability differ.

🛠️ API Design and Usage

body-scroll-lock: Modern, Flexible, and Safe

body-scroll-lock provides two main functions: disableBodyScroll and enableBodyScroll. You pass a reference to the target element (e.g., your modal) that should remain scrollable.

import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';

const modal = document.querySelector('#modal');

disableBodyScroll(modal);

// Later, when closing the modal
enableBodyScroll(modal);

It also supports options like reserveScrollBarGap to prevent layout jumps:

disableBodyScroll(modal, {
  reserveScrollBarGap: true // Adds padding to compensate for hidden scrollbar
});

The library handles edge cases such as:

  • Nested modals (via internal reference counting)
  • Touch devices (prevents pull-to-refresh and bounce)
  • Dynamic content changes

scroll-lock: Simpler but Outdated

scroll-lock used a global lock approach without requiring a target element:

// ❌ Deprecated — do not use
import { lock, unlock } from 'scroll-lock';

lock();
// ... later
unlock();

It lacked fine-grained control over which elements should remain scrollable and did not handle nested scroll contexts well. There was no built-in mechanism to preserve scroll position or compensate for scrollbar width, often causing visible layout shifts.

Because it’s deprecated, no code examples using scroll-lock should be written in production code today.

🧪 Browser Compatibility and Edge Cases

body-scroll-lock

  • Handles iOS Safari quirks (e.g., rubber-band scrolling)
  • Prevents overscroll on mobile via touchmove event listeners with { passive: false }
  • Correctly calculates scrollbar width across browsers (including Linux systems with always-visible scrollbars)
  • Supports multiple locked elements simultaneously (e.g., a modal over a drawer)

Example of how it manages touch events internally:

// Simplified internal logic
function handleTouchMove(e) {
  if (!isTargetElement(e.target)) {
    e.preventDefault(); // Blocks scroll on non-target areas
  }
}

document.addEventListener('touchmove', handleTouchMove, { passive: false });

scroll-lock

Lacked robust mobile support. On iOS, background content could still be scrolled via swipe gestures, breaking the illusion of a locked UI. It also didn’t account for dynamic changes in viewport size or scrollbar visibility.

🔄 Cleanup and Memory Safety

body-scroll-lock automatically tracks locked elements and cleans up event listeners when all locks are released. It also provides clearAllBodyScrollLocks() for emergency resets (e.g., after an unexpected navigation):

import { clearAllBodyScrollLocks } from 'body-scroll-lock';

// Useful in React useEffect cleanup or route guards
clearAllBodyScrollLocks();

scroll-lock had no such mechanism, leading to potential memory leaks or stuck scroll states if unlock() wasn’t called precisely.

🧩 Real-World Integration Example

Using body-scroll-lock in a React Modal

import { useEffect, useRef } from 'react';
import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';

function Modal({ isOpen, children }) {
  const modalRef = useRef(null);

  useEffect(() => {
    if (isOpen && modalRef.current) {
      disableBodyScroll(modalRef.current, { reserveScrollBarGap: true });
    } else if (modalRef.current) {
      enableBodyScroll(modalRef.current);
    }

    return () => {
      if (modalRef.current) {
        enableBodyScroll(modalRef.current);
      }
    };
  }, [isOpen]);

  // Emergency cleanup on unmount
  useEffect(() => {
    return () => clearAllBodyScrollLocks();
  }, []);

  if (!isOpen) return null;

  return (
    <div ref={modalRef} style={{ overflowY: 'auto' }}>
      {children}
    </div>
  );
}

This pattern ensures scroll is properly restored even if the component unmounts unexpectedly.

📌 Summary: Key Differences

Featurebody-scroll-lockscroll-lock
Maintenance Status✅ Actively maintained❌ Deprecated
Target Element Support✅ Locks body, keeps target scrollable❌ Global lock only
Scrollbar Gap FixreserveScrollBarGap option❌ Not supported
Mobile Scroll Prevention✅ Robust touch handling❌ Incomplete
Nested Locks✅ Reference counting❌ Not supported
Cleanup SafetyclearAllBodyScrollLocks()❌ Manual only

💡 Final Recommendation

  • Use body-scroll-lock for any new project requiring scroll locking. It’s reliable, well-tested, and handles modern browser quirks.
  • Avoid scroll-lock entirely. It is deprecated, lacks critical features, and may introduce bugs in production.

If you’re already using scroll-lock, migrate to body-scroll-lock as soon as possible. The migration is straightforward: replace lock()/unlock() calls with disableBodyScroll(target)/enableBodyScroll(target), and add a target reference to your overlay element.

In short: there’s only one viable choice today — and that’s body-scroll-lock.

How to Choose: body-scroll-lock vs scroll-lock
  • body-scroll-lock:

    Choose body-scroll-lock for any new project requiring scroll locking functionality. It is actively maintained, handles mobile and desktop edge cases correctly, supports nested overlays, and provides options like reserveScrollBarGap to prevent layout shifts. Its API requires passing a target element, giving you precise control over which parts of the UI remain scrollable.

  • scroll-lock:

    Do not choose scroll-lock. It is officially deprecated according to its npm page and GitHub repository, lacks support for modern browser behaviors (especially on mobile), does not handle scrollbar width compensation, and offers no mechanism for managing multiple scrollable regions. Use body-scroll-lock instead.

README for body-scroll-lock

Body scroll lock...just works with everything ;-)

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

LIGHT Package Size:

minzip size

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>;
  }
}
Angular
import { Component, ElementRef, OnDestroy, ViewChild } from "@angular/core";

// 1. Import the functions
import {
  disableBodyScroll,
  enableBodyScroll,
  clearAllBodyScrollLocks
} from "body-scroll-lock";

@Component({
  selector: "app-scroll-block",
  templateUrl: "./scroll-block.component.html",
  styleUrls: ["./scroll-block.component.css"]
})
export class SomeComponent implements OnDestroy {
  // 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.
  @ViewChild("scrollTarget") scrollTarget: ElementRef;

  showTargetElement() {
    // ... some logic to show target element

    // 3. Disable body scroll
    disableBodyScroll(this.scrollTarget.nativeElement);
  }
  
  hideTargetElement() {
    // ... some logic to hide target element

    // 4. Re-enable body scroll
    enableBodyScroll(this.scrollTarget.nativeElement);
  }

  ngOnDestroy() {
    // 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();
  }
}

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 Vercel.

Functions

FunctionArgumentsReturnDescription
disableBodyScrolltargetElement: HTMLElement
options: BodyScrollOptions
voidDisables body scroll while enabling scroll on target element
enableBodyScrolltargetElement: HTMLElementvoidEnables body scroll and removing listeners on target element
clearAllBodyScrollLocksnullvoidClears 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

To disable scrolling on iOS, disableBodyScroll prevents touchmove events. However, there are cases where you have called disableBodyScroll on an element, but its children still require touchmove events to function.

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.parentElement;
    }
  },
});

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.