mwui
GitHub

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

OptionDescription
targetElement to lock. Defaults to document.body.
widthVariableName of the CSS variable holding the scrollbar width. Defaults to --scrollbar-width.

Behavior

  • The previous inline overflow and padding-right are 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.