Enables body scroll locking for iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox without breaking scrolling of target elements like modals, lightboxes, flyouts, and nav-menus
npx @tessl/cli install tessl/npm-body-scroll-lock@3.1.0Body Scroll Lock enables body scroll locking for iOS Mobile and Tablet, Android, and desktop browsers (Safari, Chrome, Firefox) without breaking scrolling of target elements like modals, lightboxes, flyouts, and navigation menus. It provides a robust cross-platform solution that addresses limitations of basic CSS overflow approaches.
npm install body-scroll-lockimport { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';For CommonJS:
const { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } = require('body-scroll-lock');For browser/UMD:
<script src="lib/bodyScrollLock.js"></script>
<script>
// Access via global: bodyScrollLock.disableBodyScroll, etc.
</script>import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
// Get target element that should maintain scroll capability
const targetElement = document.querySelector('#modal-content');
// Show modal and disable body scroll
function showModal() {
// ... show modal logic
disableBodyScroll(targetElement);
}
// Hide modal and re-enable body scroll
function hideModal() {
// ... hide modal logic
enableBodyScroll(targetElement);
}
// Cleanup - useful for component unmounting
function cleanup() {
clearAllBodyScrollLocks();
}Body Scroll Lock uses a platform-detection approach to provide optimal scroll locking:
overflow: hidden with optional scrollbar gap reservationDisables scrolling on the document body while preserving scroll functionality for a specified target element.
/**
* Disables body scroll while enabling scroll on target element
* @param targetElement - HTMLElement that should maintain scroll capability
* @param options - Optional configuration for scroll lock behavior
*/
function disableBodyScroll(targetElement: HTMLElement, options?: BodyScrollOptions): void;Parameters:
targetElement (HTMLElement): The element that should maintain scroll capability (e.g., modal content, flyout menu)options (BodyScrollOptions, optional): Configuration options for scroll lock behaviorUsage Examples:
// Basic usage
const modal = document.querySelector('#modal');
disableBodyScroll(modal);
// With options
disableBodyScroll(modal, {
reserveScrollBarGap: true,
allowTouchMove: el => el.tagName === 'TEXTAREA'
});Re-enables body scrolling and removes scroll lock for a specific target element.
/**
* Enables body scroll and removes listeners on target element
* @param targetElement - HTMLElement to remove scroll lock from
*/
function enableBodyScroll(targetElement: HTMLElement): void;Parameters:
targetElement (HTMLElement): The element to remove scroll lock from (must match element used in disableBodyScroll)Usage Examples:
// Re-enable scrolling for specific element
const modal = document.querySelector('#modal');
enableBodyScroll(modal);Removes all scroll locks and restores normal body scrolling. Useful as a cleanup function.
/**
* Clears all scroll locks and restores normal body scrolling
*/
function clearAllBodyScrollLocks(): void;Usage Examples:
// Clear all locks (useful for cleanup)
clearAllBodyScrollLocks();
// Common pattern in React components
componentWillUnmount() {
clearAllBodyScrollLocks();
}Configuration options for customizing scroll lock behavior.
interface BodyScrollOptions {
/**
* Prevents layout shift by reserving scrollbar width as padding-right on body.
* Avoids flickering effect when body width changes due to hidden scrollbar.
*/
reserveScrollBarGap?: boolean;
/**
* Function to determine if specific elements should allow touch move events on iOS.
* Useful for nested scrollable elements within the target element.
* @param el - Element being tested for touch move allowance
* @returns true if element should allow touch move events
*/
allowTouchMove?: (el: any) => boolean;
}Usage Examples:
// Reserve scrollbar gap to prevent layout shift
disableBodyScroll(targetElement, {
reserveScrollBarGap: true
});
// Allow touch move for specific elements
disableBodyScroll(targetElement, {
allowTouchMove: el => el.tagName === 'TEXTAREA'
});
// Complex touch move filtering
disableBodyScroll(targetElement, {
allowTouchMove: el => {
// Allow scrolling for elements with special attribute
while (el && el !== document.body) {
if (el.getAttribute('body-scroll-lock-ignore') !== null) {
return true;
}
el = el.parentElement;
}
return false;
}
});ontouchstart, ontouchmove)touchmove events with passive: falseallowTouchMove filtering for nested scrollable elements-webkit-overflow-scrolling: touch) on target elementsoverflow: hidden on document bodyThe library includes built-in error handling:
clearAllBodyScrollLocks() safely handles cleanup even if no locks are active// Modal component
class Modal {
show() {
this.element.style.display = 'block';
disableBodyScroll(this.contentElement);
}
hide() {
enableBodyScroll(this.contentElement);
this.element.style.display = 'none';
}
}// Mobile navigation
const navToggle = document.querySelector('#nav-toggle');
const navMenu = document.querySelector('#nav-menu');
navToggle.addEventListener('click', () => {
if (navMenu.classList.contains('open')) {
navMenu.classList.remove('open');
enableBodyScroll(navMenu);
} else {
navMenu.classList.add('open');
disableBodyScroll(navMenu);
}
});import React, { useEffect, useRef } from 'react';
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
function Modal({ isOpen, children }) {
const contentRef = useRef();
useEffect(() => {
if (isOpen && contentRef.current) {
disableBodyScroll(contentRef.current);
}
return () => {
if (contentRef.current) {
enableBodyScroll(contentRef.current);
}
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal-content" ref={contentRef}>
{children}
</div>
</div>
);
}