# useScrollLock > Lock and restore scrolling on the body or an element. - Category: DOM - Package: `hookli` (install with `npm i hookli`) - Docs: https://hookli.vercel.app/docs/use-scroll-lock ## Signature ```ts useScrollLock(options?: UseScrollLockOptions): UseScrollLockReturn ``` ## Parameters - `options`: `UseScrollLockOptions` (default: `{}`) — Auto-lock behaviour, the lock target, and scrollbar compensation. ## Returns - `isLocked`: `boolean` — Whether the target's scroll is currently locked. - `lock`: `() => void` — Lock the target's scroll (sets overflow: hidden, optionally padding-compensated). - `unlock`: `() => void` — Restore the target's original scroll behaviour. ## Types ### UseScrollLockOptions Controls what is locked and when. - `autoLock`: `boolean` (default: `true`) — Lock automatically on mount and restore on unmount. - `lockTarget`: `HTMLElement | string` (default: ``) — Element (or CSS selector) whose scroll to lock. Defaults to the document body. - `widthReflow`: `boolean` (default: `true`) — Compensate for the removed scrollbar with padding so the layout does not shift. ### UseScrollLockReturn The current lock state plus manual controls. - `isLocked`: `boolean` — Whether the target's scroll is currently locked. - `lock`: `() => void` — Lock the target's scroll. - `unlock`: `() => void` — Restore the target's original scroll behaviour. ## Usage ```tsx import { useScrollLock } from "hookli"; export function Modal({ onClose }: { onClose: () => void }) { // Locks scroll on mount, restores it on unmount. useScrollLock(); return (
Scrolling behind this modal is frozen.
); } ``` ## Source `src/hooks/use-scroll-lock/use-scroll-lock.ts` ```ts 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(null); const originalStyle = useRef(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(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 }; }; ```