useLockedBody

Prevent body scrolling when a modal or overlay is active.

Scroll Lock Demo

Click the button below to enter "Zen Mode". This will lock the body scroll.

Body Scroll: UNLOCKED

Usage

import { useLockedBody } from 'usehooks-ts'
import { useState } from 'react'
 
export default function Modal() {
  const [locked, setLocked] = useLockedBody()
 
  return (
    <div>
      <button onClick={() => setLocked(true)}>Open Modal</button>
      
      {locked && (
        <div className="modal-overlay">
          <div className="modal-content">
            <h1>Modal Open</h1>
            <button onClick={() => setLocked(false)}>Close</button>
          </div>
        </div>
      )}
    </div>
  )
}

API

const [locked, setLocked] = useLockedBody(initialLocked?, rootId?)

Parameters

NameTypeDefaultDescription
initialLockedbooleanfalseInitial lock state
rootIdstring'root'ID of the application root element (to handle scrollbar width compensation)

Return Values

NameTypeDescription
lockedbooleanCurrent lock state
setLocked(locked: boolean) => voidFunction to toggle lock state

Hook

import { useEffect, useState } from 'react'
 
const useIsomorphicLayoutEffect =
  typeof window !== 'undefined' ? useEffect : useEffect
 
export function useLockedBody(initialLocked = false, rootId = 'root') {
  const [locked, setLocked] = useState(initialLocked)
 
  // Do the locking
  useIsomorphicLayoutEffect(() => {
    if (!locked) return
 
    // Save initial body style
    const originalOverflow = document.body.style.overflow
    const originalPaddingRight = document.body.style.paddingRight
 
    // Lock body scroll
    document.body.style.overflow = 'hidden'
 
    // Get the scrollBar width
    const root = document.getElementById(rootId) // or root
    const scrollBarWidth = root ? root.offsetWidth - root.clientWidth : 0
 
    // Avoid width reflow
    if (scrollBarWidth) {
      document.body.style.paddingRight = `${scrollBarWidth}px`
    }
 
    return () => {
      document.body.style.overflow = originalOverflow
 
      if (scrollBarWidth) {
        document.body.style.paddingRight = originalPaddingRight
      }
    }
  }, [locked, rootId])
 
  // Update state if initialLocked changes
  useEffect(() => {
    if (locked !== initialLocked) {
      setLocked(initialLocked)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [initialLocked])
 
  return [locked, setLocked] as const
}