# 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 };
};
```