Skip to content

Scroll lock

import { lockScroll, isScrollLocked } from "surface-elements/scroll-lock"
const release = lockScroll()
// …
release()

Sets overflow: hidden on <html> and returns a release function.

  • Ref-counted. The page unlocks when every caller has released. Stacked drawers each hold their own lock.
  • One count per page. The count lives on Symbol.for("surface-elements.scroll-lock"), so copy files and the package share it when they run on the same page.
  • Idempotent release. Calling a release twice is harmless.
  • No layout shift. With a classic scrollbar it sets scrollbar-gutter: stable so the page does not jump sideways.
  • Restores what it found. Inline overflow and scrollbar-gutter values you had set come back on unlock.
function lockScroll(): () => void

Whether any lock is held.

function isScrollLocked(): boolean
Two stacked drawers, one lock count

First

Open the second drawer, close it, and the page stays locked until this one closes too.

Second

Platform guidance prefers overscroll-behavior: contain on the dialog and its backdrop. Safari ignores it on containers that do not scroll, so the page behind still scrolls there. The library keeps the CSS containment and adds this lock on top. The trade-offs are in ADR 0002.