Hooks

useScrollLock

Lock scroll on document.body (or any element) without a layout jump. Sets overflow: hidden and compensates for the scrollbar width. Reference-counted per target - nested overlays locking the same element do not unlock prematurely. Original styles are restored when the last lock releases or the component unmounts. SSR-safe.

Reactive (declarative)

Pass a reactive getter - the most common pattern. Scroll locks while the getter returns true and unlocks automatically when it becomes false.

Use the Modal component - it calls useScrollLock internally.

<script lang="ts">
  import { Modal, Button } from 'sv5ui';
  import { useScrollLock } from 'sv5ui';

  let open = $state(false);

  // Scroll locks while open is true, unlocks when false
  useScrollLock(() => open);
</script>

<Button label="Open Modal" onclick={() => (open = true)} />

<Modal title="Scroll is locked" bind:open>
  {#snippet children({ props })}
    <Button {...props} label="Open Modal" onclick={() => (open = true)} />
  {/snippet}
  <p>The page scroll is locked while this modal is open.</p>
</Modal>

Imperative control

Use the returned lock and unlock methods for programmatic control. Pass false as the initial value to start unlocked.

<script lang="ts">
  import { Button } from 'sv5ui';
  import { useScrollLock } from 'sv5ui';

  // Pass false as initial state - control via lock/unlock
  const scroll = useScrollLock(false);
</script>

<div class="flex gap-2">
  <Button label="Lock scroll" onclick={() => scroll.lock()} />
  <Button label="Unlock scroll" variant="outline" onclick={() => scroll.unlock()} />
</div>

Custom target element

Pass a second argument to lock a specific scrollable element instead of document.body. Use a getter when the element is bound with bind:this.

Line 1

Line 2

Line 3

Line 4

Line 5

Line 6

Line 7

Line 8

Line 9

Line 10

Line 11

Line 12

Line 13

Line 14

Line 15

Line 16

Line 17

Line 18

Line 19

Line 20

<script lang="ts">
  import { useScrollLock } from 'sv5ui';

  let panel = $state<HTMLDivElement>();
  let locked = $state(false);

  // Lock a scrollable panel instead of document.body
  useScrollLock(() => locked, () => panel);
</script>

<button onclick={() => (locked = !locked)}>
  {locked ? 'Unlock panel' : 'Lock panel'}
</button>

<div bind:this={panel} class="h-40 overflow-y-auto rounded border p-2" style="overflow-y: scroll;">
  {#each Array(20) as _, i}
    <p>Line {i + 1}</p>
  {/each}
</div>

Parameters

ParameterTypeDefault
lockedboolean | (() => boolean)-
targetHTMLElement | (() => HTMLElement | null) | nulldocument.body

Return

PropertyType-
lock() => void-
unlock() => void-