DOM

useScrollLock

Lock and restore scrolling on the body or an element.

Demo

  • Row 1
  • Row 2
  • Row 3
  • Row 4
  • Row 5
  • Row 6
  • Row 7
  • Row 8
  • Row 9
  • Row 10
  • Row 11
  • Row 12
isLocked
false

Lock the panel, then try scrolling the list — perfect for freezing the page behind an open modal or drawer.

Usage

use-scroll-lock.tsxtsx
import { useScrollLock } from "hookli";

export function Modal({ onClose }: { onClose: () => void }) {
  // Locks <body> scroll on mount, restores it on unmount.
  useScrollLock();

  return (
    <div role="dialog" onClick={onClose}>
      Scrolling behind this modal is frozen.
    </div>
  );
}

API

useScrollLock.d.tsts
useScrollLock(options?: UseScrollLockOptions): UseScrollLockReturn

Parameters

NameTypeDefaultDescription
optionsUseScrollLockOptions{}Auto-lock behaviour, the lock target, and scrollbar compensation.

Returns

NameTypeDescription
isLockedbooleanWhether the target's scroll is currently locked.
lock() => voidLock the target's scroll (sets overflow: hidden, optionally padding-compensated).
unlock() => voidRestore the target's original scroll behaviour.

UseScrollLockOptions

Controls what is locked and when.

NameTypeDescription
autoLockbooleanLock automatically on mount and restore on unmount.
lockTargetHTMLElement | stringElement (or CSS selector) whose scroll to lock. Defaults to the document body.
widthReflowbooleanCompensate for the removed scrollbar with padding so the layout does not shift.

UseScrollLockReturn

The current lock state plus manual controls.

NameTypeDescription
isLockedbooleanWhether the target's scroll is currently locked.
lock() => voidLock the target's scroll.
unlock() => voidRestore the target's original scroll behaviour.

Hook

The implementation, straight from hookli. Copy it, or install the package.

src/hooks/use-scroll-lock/use-scroll-lock.tstsx
import { useCallback, useRef, useState } from "react";
import { useIsomorphicLayoutEffect } from "../use-isomorphic-layout-effect/use-isomorphic-layout-effect";

interface UseScrollLockOptions {
  autoLock?: boolean;
  lockTarget?: HTMLElement | string;
  widthReflow?: boolean;
}

interface UseScrollLockReturn {
  isLocked: boolean;
  lock: () => void;
  unlock: () => void;
}

interface OriginalStyle {
  overflow: string;
  paddingRight: string;
}

export const useScrollLock = (
  options: UseScrollLockOptions = {},
): UseScrollLockReturn => {
  const { autoLock = true, lockTarget, widthReflow = true } = options;
  const [isLocked, setIsLocked] = useState(false);
  const target = useRef<HTMLElement | null>(null);
  const originalStyle = useRef<OriginalStyle | null>(null);

  const resolveTarget = useCallback((): HTMLElement | null => {
    if (typeof document === "undefined") return null;
    if (lockTarget instanceof HTMLElement) return lockTarget;
    if (typeof lockTarget === "string") {
      return document.querySelector<HTMLElement>(lockTarget);
    }
    return document.body;
  }, [lockTarget]);

  const lock = useCallback(() => {
    const node = resolveTarget();
    if (!node) return;

    target.current = node;
    originalStyle.current = {
      overflow: node.style.overflow,
      paddingRight: node.style.paddingRight,
    };

    if (widthReflow && typeof window !== "undefined") {
      const scrollbarWidth = window.innerWidth - node.clientWidth;
      if (scrollbarWidth > 0) {
        const currentPadding =
          parseInt(window.getComputedStyle(node).paddingRight, 10) || 0;
        node.style.paddingRight = `${currentPadding + scrollbarWidth}px`;
      }
    }

    node.style.overflow = "hidden";
    setIsLocked(true);
  }, [resolveTarget, widthReflow]);

  const unlock = useCallback(() => {
    const node = target.current;
    if (!node || !originalStyle.current) return;

    node.style.overflow = originalStyle.current.overflow;
    node.style.paddingRight = originalStyle.current.paddingRight;
    originalStyle.current = null;
    setIsLocked(false);
  }, []);

  useIsomorphicLayoutEffect(() => {
    if (!autoLock) return;
    lock();
    return () => {
      unlock();
    };
  }, [autoLock, lock, unlock]);

  return { isLocked, lock, unlock };
};

View source on GitHub

Related hooks