Shadcn Hooks

useScrollLock

A hook to lock the scroll of the body

Loading...

Installation

npx shadcn@latest add @hooks/use-scroll-lock
pnpm dlx shadcn@latest add @hooks/use-scroll-lock
yarn dlx shadcn@latest add @hooks/use-scroll-lock
bun x shadcn@latest add @hooks/use-scroll-lock

Copy and paste the following code into your project.

use-isomorphic-layout-effect.ts
import { useEffect, useLayoutEffect } from 'react'
import { isBrowser } from '@/registry/lib/is-browser'

/**
 * Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
 * @param {Function} effect - The effect function to be executed.
 * @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
 * @public
 * @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect)
 * @example
 * ```tsx
 * useIsomorphicLayoutEffect(() => {
 *   // Code to be executed during the layout phase on the client side
 * }, [dependency1, dependency2]);
 * ```
 */
export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect

is-browser.ts
/**
 * A library that checks if the code is running in a browser.
 */
export const isBrowser = typeof window !== 'undefined'

use-scroll-lock.ts
import { useCallback, useRef, useState } from 'react'
import { useIsomorphicLayoutEffect } from '@/registry/hooks/use-isomorphic-layout-effect'
import { isBrowser } from '@/registry/lib/is-browser'

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

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

interface OriginalStyle {
  overflow: CSSStyleDeclaration['overflow']
  paddingRight: CSSStyleDeclaration['paddingRight']
}

export 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 = useCallback(() => {
    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 =
          Number.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)
    }
  }, [widthReflow])

  const unlock = useCallback(() => {
    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)
  }, [widthReflow])

  useIsomorphicLayoutEffect(() => {
    if (!isBrowser) return

    if (lockTarget) {
      target.current =
        typeof lockTarget === 'string'
          ? document.querySelector(lockTarget)
          : lockTarget
    }

    if (!target.current) {
      target.current = document.body
    }

    if (autoLock) {
      lock()
    }

    return () => {
      unlock()
    }
  }, [autoLock, lockTarget, widthReflow])

  return { isLocked, lock, unlock }
}

API

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

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

/**
 * A hook to lock the scroll of the body
 * @param options - The options for the hook
 * @param options.autoLock - Whether to automatically lock the scroll when the component is mounted
 * @param options.lockTarget - The target element to lock the scroll on
 * @param options.widthReflow - Whether to reflow the width of the body when the scroll is locked
 * @returns The scroll lock state and the functions to lock and unlock the scroll
 */
function useScrollLock(options?: UseScrollLockOptions): UseScrollLockReturn

Credits

Last updated on

On this page