useScrollLock
Hook to handle scroll locking.
Example
import useScrollLock from "@/hooks/useScrollLock";
export default function UseScrollLockExample() {
const { isLocked, lock, unlock } = useScrollLock({
autoLock: false,
});
return (
<div className="flex flex-col items-center gap-4 text-ctp-text">
<p>Is locked: {isLocked ? "true" : "false"}</p>
<button
className="rounded bg-ctp-blue px-4 py-2 text-ctp-base"
onClick={lock}
>
Lock
</button>
<button
className="rounded bg-ctp-pink px-4 py-2 text-ctp-base"
onClick={unlock}
>
Unlock
</button>
</div>
);
}
Code
import { useEffect, useLayoutEffect, useRef, useState } from "react";
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
type UseScrollLockOptions = {
autoLock?: boolean;
lockTarget?: HTMLElement | string;
widthReflow?: boolean;
};
type UseScrollLockReturn = {
isLocked: boolean;
lock: () => void;
unlock: () => void;
};
type OriginalStyle = {
overflow: CSSStyleDeclaration["overflow"];
paddingRight: CSSStyleDeclaration["paddingRight"];
};
const IS_SERVER = typeof window === "undefined";
/**
* Hook to handle scroll locking.
* @param {UseScrollLockOptions} options - The options for the hook.
* @returns {UseScrollLockReturn} An object containing the current state of the scroll lock and functions to control it.
*/
export default function 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 lock = () => {
if (target.current) {
const { overflow, paddingRight } = target.current.style;
// Save the original styles
originalStyle.current = { overflow, paddingRight };
// Prevent width reflow
if (widthReflow) {
// Use window inner width if body is the target as global scrollbar isn't part of the document
const offsetWidth =
target.current === document.body
? window.innerWidth
: target.current.offsetWidth;
// Get current computed padding right in pixels
const currentPaddingRight =
parseInt(window.getComputedStyle(target.current).paddingRight, 10) ||
0;
const scrollbarWidth = offsetWidth - target.current.scrollWidth;
target.current.style.paddingRight = `${scrollbarWidth + currentPaddingRight}px`;
}
// Lock the scroll
target.current.style.overflow = "hidden";
setIsLocked(true);
}
};
const unlock = () => {
if (target.current && originalStyle.current) {
target.current.style.overflow = originalStyle.current.overflow;
// Only reset padding right if we changed it
if (widthReflow) {
target.current.style.paddingRight = originalStyle.current.paddingRight;
}
}
setIsLocked(false);
};
useIsomorphicLayoutEffect(() => {
if (IS_SERVER) return;
if (lockTarget) {
target.current =
typeof lockTarget === "string"
? document.querySelector(lockTarget)
: lockTarget;
}
if (!target.current) {
target.current = document.body;
}
if (autoLock) {
lock();
}
return () => {
unlock();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoLock, lockTarget, widthReflow]);
return { isLocked, lock, unlock };
}