React component that disables scroll outside of children node while maintaining scroll functionality within.
npx @tessl/cli install tessl/npm-react-remove-scroll@2.7.0React Remove Scroll is a TypeScript React library that prevents scrolling outside of specified child elements while maintaining scroll functionality within those elements. It's designed for modals, dropdowns, and overlay components where background scrolling should be disabled.
npm install react-remove-scrollimport { RemoveScroll } from "react-remove-scroll";CommonJS:
const { RemoveScroll } = require("react-remove-scroll");Alternative imports for bundle splitting:
// UI-only component (400 bytes)
import { RemoveScroll } from "react-remove-scroll/UI";
import sidecar from "react-remove-scroll/sidecar";
<RemoveScroll sideCar={sidecar}>Content</RemoveScroll>import { RemoveScroll } from "react-remove-scroll";
function Modal({ isOpen, children }) {
return (
<RemoveScroll enabled={isOpen}>
{children}
</RemoveScroll>
);
}
// Basic scroll prevention
<RemoveScroll>
<div>Only this content is scrollable</div>
</RemoveScroll>React Remove Scroll uses a layered architecture:
RemoveScroll - combines UI and side effects through a sidecar patternMain scroll prevention component with comprehensive configuration options.
/**
* React component that prevents scrolling outside of its children while maintaining
* scroll functionality within the children. Supports both container and forward-props modes.
*/
const RemoveScroll: React.ForwardRefExoticComponent<
IRemoveScrollProps & React.RefAttributes<HTMLElement>
> & {
classNames: {
fullWidth: string;
zeroRight: string;
};
};
type IRemoveScrollProps = IRemoveScrollSelfProps & (ChildrenNode | ChildrenForward);
interface IRemoveScrollSelfProps {
/** Enable/disable scroll lock behavior */
enabled?: boolean; // default: true
/** Control removal of document scrollbar */
removeScrollBar?: boolean; // default: true
/** Allow pinch-to-zoom gestures (may break scroll isolation) */
allowPinchZoom?: boolean; // default: false
/** Prevent setting position:relative on body */
noRelative?: boolean; // default: false
/** Disable event isolation outside the lock */
noIsolation?: boolean; // default: false
/** Use pointer-events:none for complete page isolation */
inert?: boolean; // default: false
/** Additional elements to include in the scroll lock - array of refs or DOM elements that should remain interactive */
shards?: Array<React.RefObject<any> | HTMLElement>;
/** Container element type when not using forwardProps */
as?: string | React.ElementType; // default: 'div'
/** Strategy for filling scrollbar gap - 'margin' adds margin, 'padding' adds padding */
gapMode?: 'padding' | 'margin'; // default: 'margin'
/** CSS class for container */
className?: string;
/** Inline styles for container */
style?: React.CSSProperties;
/** Forwarded ref to the container element */
ref?: React.Ref<HTMLElement>;
}
interface ChildrenNode {
/** Wrap children in a container element */
forwardProps?: false;
children: React.ReactNode;
}
interface ChildrenForward {
/** Forward props directly to the single child element */
forwardProps: true;
children: React.ReactElement;
}Usage Examples:
import { RemoveScroll } from "react-remove-scroll";
// Container mode (default)
<RemoveScroll className="modal-container">
<div>Scrollable content</div>
</RemoveScroll>
// Forward props mode
<RemoveScroll forwardProps>
<div className="custom-container">
Scrollable content
</div>
</RemoveScroll>
// Advanced configuration
<RemoveScroll
enabled={isModalOpen}
allowPinchZoom={true}
removeScrollBar={true}
shards={[buttonRef, headerRef]}
gapMode="padding"
>
<div>Modal content</div>
</RemoveScroll>CSS class names for handling position:fixed elements when scroll lock is active.
RemoveScroll.classNames: {
/** Class for full-width fixed elements */
fullWidth: string;
/** Class for right-aligned fixed elements */
zeroRight: string;
};Usage Examples:
import { RemoveScroll } from "react-remove-scroll";
import cx from "classnames";
// Full-width fixed header
<header className={cx("fixed-header", RemoveScroll.classNames.fullWidth)}>
Header content
</header>
// Right-aligned fixed sidebar
<aside className={cx("fixed-sidebar", RemoveScroll.classNames.zeroRight)}>
Sidebar content
</aside>Separate UI and sidecar components for optimized bundle loading.
/**
* UI-only component requiring a sidecar for side effects
* Import from: react-remove-scroll/UI
*/
const RemoveScroll: React.ForwardRefExoticComponent<
IRemoveScrollUIProps & React.RefAttributes<HTMLElement>
>;
interface IRemoveScrollUIProps extends IRemoveScrollProps {
/** Side effect component for scroll prevention logic */
sideCar: React.FC<any>;
}
/**
* Sidecar component containing scroll prevention side effects
* Import from: react-remove-scroll/sidecar
*/
const sidecar: React.FC;Usage Examples:
import { RemoveScroll } from "react-remove-scroll/UI";
import sidecar from "react-remove-scroll/sidecar";
// Manual sidecar usage
<RemoveScroll sideCar={sidecar}>
<div>Content with side effects</div>
</RemoveScroll>
// Dynamic sidecar loading using use-sidecar
import { sidecar } from "use-sidecar";
const dynamicSidecar = sidecar(() => import('react-remove-scroll/sidecar'));
<RemoveScroll sideCar={dynamicSidecar}>
<div>Content with dynamically loaded side effects</div>
</RemoveScroll>Shards allow additional DOM elements to remain interactive while scroll is locked:
import { useRef } from 'react';
import { RemoveScroll } from 'react-remove-scroll';
function ModalWithShards() {
const buttonRef = useRef<HTMLButtonElement>(null);
const headerRef = useRef<HTMLElement>(null);
return (
<>
{/* These elements remain interactive due to shards */}
<button ref={buttonRef}>Close Modal</button>
<header ref={headerRef}>App Header</header>
<RemoveScroll shards={[buttonRef, headerRef]} enabled={isModalOpen}>
<div>Modal content - scroll is locked everywhere else</div>
</RemoveScroll>
</>
);
}// margin mode (default) - adds margin-right to body to compensate for removed scrollbar
<RemoveScroll gapMode="margin">
<div>Content</div>
</RemoveScroll>
// padding mode - adds padding-right to body to compensate for removed scrollbar
<RemoveScroll gapMode="padding">
<div>Content</div>
</RemoveScroll>// For better performance on large scrollable areas - disables event isolation
<RemoveScroll noIsolation>
<div>Large scrollable content</div>
</RemoveScroll>
// Complete isolation using pointer-events (use carefully - not portal-friendly)
<RemoveScroll inert>
<div>Modal content</div>
</RemoveScroll>/** Scroll axis direction */
type Axis = 'v' | 'h';
/** Strategy for filling scrollbar gap */
type GapMode = 'padding' | 'margin';
/** Event handling callbacks for scroll prevention - internal use by sidecar */
interface RemoveScrollEffectCallbacks {
/** Handles scroll events on the locked element */
onScrollCapture(event: Event): void;
/** Handles wheel events for mouse scroll prevention */
onWheelCapture(event: WheelEvent): void;
/** Handles touch move events for touch scroll prevention */
onTouchMoveCapture(event: TouchEvent): void;
}
/** Internal props interface for side effect component */
interface IRemoveScrollEffectProps {
/** Prevent setting position:relative on body */
noRelative?: boolean;
/** Disable event isolation outside the lock */
noIsolation?: boolean;
/** Control removal of document scrollbar */
removeScrollBar?: boolean;
/** Allow pinch-to-zoom gestures */
allowPinchZoom: boolean;
/** Use pointer-events:none for complete page isolation */
inert?: boolean;
/** Additional elements to include in the scroll lock */
shards?: Array<React.RefObject<any> | HTMLElement>;
/** Reference to the lock container element */
lockRef: React.RefObject<HTMLElement>;
/** Strategy for filling scrollbar gap */
gapMode?: GapMode;
/** Callback to set event handlers */
setCallbacks(cb: RemoveScrollEffectCallbacks): void;
}
/** Component type with static properties */
type RemoveScrollType = React.ForwardRefExoticComponent<
IRemoveScrollProps & React.RefAttributes<HTMLElement>
> & {
classNames: {
fullWidth: string;
zeroRight: string;
};
};React Remove Scroll handles common edge cases automatically:
Common issues:
noIsolation mode for better performance.inert prop is not React portal-friendly and may cause issues in production.allowPinchZoom may break scroll isolation in some scenarios.