DOM
useScrollLock
Lock and restore scrolling on the body or an element.
Demo
live · imported from hookli
- 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.
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>
);
}Usage
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(options?: UseScrollLockOptions): UseScrollLockReturnParameters
| Name | Type | Default | Description |
|---|---|---|---|
| options | UseScrollLockOptions | {} | Auto-lock behaviour, the lock target, and scrollbar compensation. |
Returns
| Name | Type | Description |
|---|---|---|
| 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. |
UseScrollLockOptions
Controls what is locked and when.
| Name | Type | Description |
|---|---|---|
| autoLock | boolean | Lock automatically on mount and restore on unmount. |
| lockTarget | HTMLElement | string | Element (or CSS selector) whose scroll to lock. Defaults to the document body. |
| widthReflow | boolean | Compensate for the removed scrollbar with padding so the layout does not shift. |
UseScrollLockReturn
The current lock state plus manual controls.
| Name | Type | Description |
|---|---|---|
| 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. |
Hook
The implementation, straight from hookli. Copy it, or install the package.
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 };
};