useScrollLock

A hook that allows you to disable scrolling on an HTML element.

Source Code

import { RefObject, useEffect, useRef } from "react";
 
export type ScrollLockTarget =
  | string
  | HTMLElement
  | RefObject<HTMLElement>
  | undefined;
 
const getTargetElement = (target: ScrollLockTarget): HTMLElement | null => {
  if (!target) return document.body;
  if (target instanceof HTMLElement) return target;
  if (typeof target === "string") return document.querySelector(target);
  if (target && "current" in target) return target.current || null;
 
  return null;
};
 
interface StyleBackup {
  overflow: string;
  scrollbarGutter: string;
}
 
const styleBackupMap = new WeakMap<HTMLElement, StyleBackup>();
const lockCounterMap = new WeakMap<HTMLElement, number>();
 
export const useScrollLock = (isLocked: boolean, target?: ScrollLockTarget) => {
  const targetElementRef = useRef<HTMLElement | null>(null);
 
  useEffect(() => {
    targetElementRef.current = getTargetElement(target);
  }, [target]);
 
  useEffect(() => {
    const targetElement = targetElementRef.current;
    if (!targetElement || !isLocked) return;
 
    const currentCount = lockCounterMap.get(targetElement) || 0;
 
    // Backup styles before first lock
    if (currentCount === 0) {
      styleBackupMap.set(targetElement, {
        overflow: targetElement.style.overflow,
        scrollbarGutter: targetElement.style.scrollbarGutter,
      });
    }
 
    const newCount = currentCount + 1;
    lockCounterMap.set(targetElement, newCount);
 
    Object.assign(targetElement.style, {
      overflow: "hidden",
      scrollbarGutter: "stable",
    });
 
    return () => {
      const currentCount = lockCounterMap.get(targetElement) || 0;
      const newCount = Math.max(0, currentCount - 1);
      lockCounterMap.set(targetElement, newCount);
 
      if (newCount === 0) {
        const previousStyles = styleBackupMap.get(targetElement) || {
          overflow: "",
          scrollbarGutter: "",
        };
 
        Object.assign(targetElement.style, previousStyles);
        styleBackupMap.delete(targetElement);
      }
    };
  }, [isLocked]);
};

Features

  • No Layout Shift - Forces scrollbar-gutter to stable to prevent layout shifts when the scroll is locked.
  • No Side Effects - The hook only adds styles to the target element and removes them (reverting to the previous styles) when the hook is unmounted.
  • Custom Target - Allows you to lock the scroll on a specific element or the entire page.

API Reference

PropDefaultTypeDescription

isLocked

*
-

boolean

Whether to lock the scroll.

target

-

string | HTMLElement | RefObject<HTMLElement>

The target element to lock the scroll on. Can be a string (selector), HTMLElement, or a RefObject with a HTMLElement.

Examples

Basic

With Custom Target