useScrollLock
A hook that locks body scroll without layout shift.
Lock it, then try scrolling the docs page.
Installation
Usage
When a modal, drawer, or full-screen menu opens, the page behind it should stop scrolling. Setting
overflow: hidden on <body> does that — and on desktop it also removes the scrollbar, which
shifts the whole layout a few pixels to the right. This hook compensates for the missing scrollbar
so nothing moves.
import { useScrollLock } from "@/hooks/use-scroll-lock";
function Drawer({ open, children }: DrawerProps) {
useScrollLock(open);
return open ? <div className="fixed inset-0">{children}</div> : null;
}Pass the open state directly — the hook is a no-op while it is false, so there is no conditional
hook call and no separate cleanup path.
Nested locks
Locks are reference counted per element. A dialog that opens a confirmation on top of itself will lock twice and unlock twice; scrolling is restored only when the last consumer releases it. Without counting, closing the inner dialog would unlock the page while the outer one is still open.
Fixed elements
While locked, the measured scrollbar width is published as a CSS variable on the locked element, so position-fixed chrome can compensate too:
.app-header {
padding-right: var(--scrollbar-width, 0px);
}Options
| Option | Description |
|---|---|
target | Element to lock. Defaults to document.body. |
widthVariable | Name of the CSS variable holding the scrollbar width. Defaults to --scrollbar-width. |
Behavior
- The previous inline
overflowandpadding-rightare captured before locking and written back on release, so an element that already had inline styles keeps them. - Padding is added on top of the computed value rather than replacing it.
- Everything runs in an effect, so server rendering is unaffected.