useLockedBody()

This React hook is used to block the scrolling of the page.

A good example of a use case is when you need to open a modal.


For flexibility, this hook offers 2 APIs:

  • Use it as we would use a useState (example 1)
  • Use it with our own logic, coming from a props or redux for example (example 2)

Finally, you can optionally change the reflow padding if you have another sidebar size than the default (15px)

The Hook

1import { useEffect, useState } from 'react'
2
3import { useIsomorphicLayoutEffect } from 'usehooks-ts'
4
5type UseLockedBodyOutput = [boolean, (locked: boolean) => void]
6
7function useLockedBody(
8 initialLocked = false,
9 rootId = '___gatsby', // Default to `___gatsby` to not introduce breaking change
10): UseLockedBodyOutput {
11 const [locked, setLocked] = useState(initialLocked)
12
13 // Do the side effect before render
14 useIsomorphicLayoutEffect(() => {
15 if (!locked) {
16 return
17 }
18
19 // Save initial body style
20 const originalOverflow = document.body.style.overflow
21 const originalPaddingRight = document.body.style.paddingRight
22
23 // Lock body scroll
24 document.body.style.overflow = 'hidden'
25
26 // Get the scrollBar width
27 const root = document.getElementById(rootId) // or root
28 const scrollBarWidth = root ? root.offsetWidth - root.scrollWidth : 0
29
30 // Avoid width reflow
31 if (scrollBarWidth) {
32 document.body.style.paddingRight = `${scrollBarWidth}px`
33 }
34
35 return () => {
36 document.body.style.overflow = originalOverflow
37
38 if (scrollBarWidth) {
39 document.body.style.paddingRight = originalPaddingRight
40 }
41 }
42 }, [locked])
43
44 // Update state if initialValue changes
45 useEffect(() => {
46 if (locked !== initialLocked) {
47 setLocked(initialLocked)
48 }
49 // eslint-disable-next-line react-hooks/exhaustive-deps
50 }, [initialLocked])
51
52 return [locked, setLocked]
53}
54
55export default useLockedBody

Usage

1import { CSSProperties, useState } from 'react'
2
3import { useLockedBody } from 'usehooks-ts'
4
5const fixedCenterStyle: CSSProperties = {
6 position: 'fixed',
7 top: '50%',
8 left: '50%',
9 transform: 'translate(-50%, -50%)',
10}
11
12const fakeScrollableStyle: CSSProperties = {
13 minHeight: '150vh',
14 background: 'linear-gradient(palegreen, palegoldenrod, palevioletred)',
15}
16
17// Example 1: useLockedBody as useState()
18export default function App() {
19 const [locked, setLocked] = useLockedBody(false, 'root')
20
21 const toggleLocked = () => {
22 setLocked(!locked)
23 }
24
25 return (
26 <div style={fakeScrollableStyle}>
27 <button style={fixedCenterStyle} onClick={toggleLocked}>
28 {locked ? 'unlock scroll' : 'lock scroll'}
29 </button>
30 </div>
31 )
32}
33
34// Example 2: useLockedBody with our custom state
35export function App2() {
36 const [locked, setLocked] = useState(false)
37
38 const toggleLocked = () => {
39 setLocked(!locked)
40 }
41
42 useLockedBody(locked, 'root')
43
44 return (
45 <div style={fakeScrollableStyle}>
46 <button style={fixedCenterStyle} onClick={toggleLocked}>
47 {locked ? 'unlock scroll' : 'lock scroll'}
48 </button>
49 </div>
50 )
51}

Edit on CodeSandbox

See a way to make this page better?
Edit there »