svelte-intersection-observer

svelte-intersection-observer

About

svelte-intersection-observer wraps the Intersection Observer API. It tells you when an element enters or leaves the viewport, without scroll listeners. Typical uses include lazy-loading, scroll animations, infinite scroll, autoplaying video, and impression tracking. See Use cases.

Six exports, pick by how you want to wire it up:

KindExportUse it when...
ComponentIntersectionObserveryou want a component with a bound intersecting prop
Pooled componentMultipleIntersectionObserveryou're observing many elements and want one shared observer
Actionintersectyou want use: on a plain element, no extra markup
AttachmentintersectAttachmentsame as the action, composed via {@attach} (Svelte 5.29+)
ComposablecreateIntersectionObserveryou want reactive state from <script>, no directive
FactorycreateIntersectionGroupyou're rendering a list with {@attach} and want one shared observer

Full docs for each are under API. Try it in the Svelte REPL.

Compatibility

Package versionSvelte versionNotes
1.x3, 4, 5 (legacy/non-runes)Uses export let, slots, and on: events
2.x≥5.29 (legacy + runes)Uses $props(), snippets, and callback props

Internals use runes. A legacy (non-runes) Svelte 5 component can still consume them: bind:, callback props, snippets, and {@attach} work across that boundary. One exception is createIntersectionObserver. It exposes intersecting/entry as plain getters, with no bind: or callback prop. Legacy templates do not reactively track getter reads, so the UI only updates if the consuming component is in runes mode.

Installation

# NPM
npm i svelte-intersection-observer

# pnpm
pnpm i svelte-intersection-observer

# Bun
bun i svelte-intersection-observer

# Yarn
yarn add svelte-intersection-observer

API

Every export takes the same core options (root, rootMargin, threshold, once, skip) and the same onobserve/onintersect/onexit callbacks. What changes is how you attach it in markup. Use cases shows concrete setups.

Callbacks onobserve onintersect and onexit

All six exports expose these three callbacks. Each receives an IntersectionObserverEntry. Components pass the entry directly; the action and attachment put it on event.detail.

<IntersectionObserver
  {element}
  onobserve={(entry) => {
    console.log(entry); // IntersectionObserverEntry
    console.log(entry.isIntersecting); // true | false
  }}
  onintersect={(entry) => {
    console.log(entry.isIntersecting); // always true
  }}
  onexit={(entry) => {
    console.log(entry.isIntersecting); // always false
  }}
>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>

IntersectionObserver

Pass an element with bind:this, then bind intersecting to know whether it is in the viewport.

Element is not in view
Hello world
<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";

  let element: HTMLElement | undefined = $state();
  let intersecting = $state(false);
</script>

<header class:intersecting>
  {intersecting ? "Element is in view" : "Element is not in view"}
</header>

<IntersectionObserver {element} bind:intersecting>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>
<script>
  import IntersectionObserver from "svelte-intersection-observer";

  let element = $state();
  let intersecting = $state(false);
</script>

<header class:intersecting>
  {intersecting ? "Element is in view" : "Element is not in view"}
</header>

<IntersectionObserver {element} bind:intersecting>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>

once

Set once to true to unobserve after the first intersection. Good for a one-shot reveal, lazy-load, or analytics impression.

Element is not in view
Hello world
<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";

  let elementOnce: HTMLElement | undefined = $state();
  let intersectOnce = $state(false);
</script>

<header class:intersecting={intersectOnce}>
  {intersectOnce ? "Element is in view" : "Element is not in view"}
</header>

<IntersectionObserver
  once
  element={elementOnce}
  bind:intersecting={intersectOnce}
>
  <div bind:this={elementOnce}>Hello world</div>
</IntersectionObserver>
<script>
  import IntersectionObserver from "svelte-intersection-observer";

  let elementOnce = $state();
  let intersectOnce = $state(false);
</script>

<header class:intersecting={intersectOnce}>
  {intersectOnce ? "Element is in view" : "Element is not in view"}
</header>

<IntersectionObserver
  once
  element={elementOnce}
  bind:intersecting={intersectOnce}
>
  <div bind:this={elementOnce}>Hello world</div>
</IntersectionObserver>

children snippet

Instead of binding intersecting, you can use the children snippet. It receives intersecting, entry, and observer.

Here, "Hello world" fades in when its container enters the viewport.

<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";
  import { fade } from "svelte/transition";

  let node: HTMLElement | undefined = $state();
</script>

<header></header>

<IntersectionObserver element={node}>
  {#snippet children({ intersecting })}
    <div bind:this={node}>
      {#if intersecting}
        <div transition:fade={{ delay: 200 }}>Hello world</div>
      {/if}
    </div>
  {/snippet}
</IntersectionObserver>
<script>
  import IntersectionObserver from "svelte-intersection-observer";
  import { fade } from "svelte/transition";

  let node = $state();
</script>

<header></header>

<IntersectionObserver element={node}>
  {#snippet children({ intersecting })}
    <div bind:this={node}>
      {#if intersecting}
        <div transition:fade={{ delay: 200 }}>Hello world</div>
      {/if}
    </div>
  {/snippet}
</IntersectionObserver>

Props

NameDescriptionTypeDefault value
elementObserved elementnull | undefined | Elementnull
onceUnobserve the element after the first intersection eventbooleanfalse
intersectingtrue if the observed element is intersecting the viewportbooleanfalse
rootContaining elementElement | Document | nullnull
rootMarginMargin offset of the containing elementstring"0px"
thresholdPercentage of element visibility to trigger an eventnumber between 0 and 1, or an array of numbers between 0 and 10
entryObserved element metadatanull or IntersectionObserverEntrynull
observerIntersectionObserver instancenull or IntersectionObservernull
skipPause observing without losing entry/intersecting statebooleanfalse

Note: for threshold values above 0, the observed element needs a non-zero width and height. That comes from the Intersection Observer API, not this component.

Callback props

Same onobserve/onintersect/onexit behavior as described in Callbacks above.

children snippet props

NameType
intersectingboolean
entrynull or IntersectionObserverEntry
observerIntersectionObserver

MultipleIntersectionObserver

Use MultipleIntersectionObserver when you have many elements and want one shared observer instead of one per element.

Item 1: ✗
Item 2: ✗
Item 1
Item 2
<script lang="ts">
  import { MultipleIntersectionObserver } from "svelte-intersection-observer";

  let ref1: HTMLElement | undefined = $state();
  let ref2: HTMLElement | undefined = $state();
  let elements = $derived([ref1, ref2]);
</script>

<MultipleIntersectionObserver {elements}>
  {#snippet children({ elementIntersections })}
    <header>
      <div class:intersecting={elementIntersections.get(ref1)}>
        Item 1: {elementIntersections.get(ref1) ? "✓" : "✗"}
      </div>
      <div class:intersecting={elementIntersections.get(ref2)}>
        Item 2: {elementIntersections.get(ref2) ? "✓" : "✗"}
      </div>
    </header>

    <div bind:this={ref1}>Item 1</div>
    <div bind:this={ref2}>Item 2</div>
  {/snippet}
</MultipleIntersectionObserver>
<script>
  import { MultipleIntersectionObserver } from "svelte-intersection-observer";

  let ref1 = $state();
  let ref2 = $state();
  let elements = $derived([ref1, ref2]);
</script>

<MultipleIntersectionObserver {elements}>
  {#snippet children({ elementIntersections })}
    <header>
      <div class:intersecting={elementIntersections.get(ref1)}>
        Item 1: {elementIntersections.get(ref1) ? "✓" : "✗"}
      </div>
      <div class:intersecting={elementIntersections.get(ref2)}>
        Item 2: {elementIntersections.get(ref2) ? "✓" : "✗"}
      </div>
    </header>

    <div bind:this={ref1}>Item 1</div>
    <div bind:this={ref2}>Item 2</div>
  {/snippet}
</MultipleIntersectionObserver>

Using with #each

MultipleIntersectionObserver works with a dynamic #each list. Give every item its own slot in an array or object; do not reuse one shared variable.

Item 1: ✗
Item 2: ✗
Item 3: ✗
Item 4: ✗
Item 5: ✗
Item 1
Item 2
Item 3
Item 4
Item 5
<script lang="ts">
  import { MultipleIntersectionObserver } from "svelte-intersection-observer";

  let items = Array.from({ length: 5 }, (_, i) => ({
    id: i + 1,
    text: `Item ${i + 1}`,
  }));

  let refs: (HTMLElement | undefined)[] = $state([]);
  let itemsContainer: HTMLElement | undefined = $state();
</script>

<MultipleIntersectionObserver elements={refs} root={itemsContainer}>
  {#snippet children({ elementIntersections })}
    <header>
      {#each items as item, i (item.id)}
        <div class:intersecting={elementIntersections.get(refs[i])}>
          {item.text}: {elementIntersections.get(refs[i]) ? "✓" : "✗"}
        </div>
      {/each}
    </header>

    <div
      bind:this={itemsContainer}
      style="height: 150px; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem;"
    >
      {#each items as item, i (item.id)}
        <div
          bind:this={refs[i]}
          style="height: 100px; display: flex; align-items: center; flex-shrink: 0;"
        >
          {item.text}
        </div>
      {/each}
    </div>
  {/snippet}
</MultipleIntersectionObserver>
<script>
  import { MultipleIntersectionObserver } from "svelte-intersection-observer";

  let items = Array.from({ length: 5 }, (_, i) => ({
    id: i + 1,
    text: `Item ${i + 1}`,
  }));

  let refs = $state([]);
  let itemsContainer = $state();
</script>

<MultipleIntersectionObserver elements={refs} root={itemsContainer}>
  {#snippet children({ elementIntersections })}
    <header>
      {#each items as item, i (item.id)}
        <div class:intersecting={elementIntersections.get(refs[i])}>
          {item.text}: {elementIntersections.get(refs[i]) ? "✓" : "✗"}
        </div>
      {/each}
    </header>

    <div
      bind:this={itemsContainer}
      style="height: 150px; overflow-y: auto; display: flex; flex-direction: column; gap: 1rem;"
    >
      {#each items as item, i (item.id)}
        <div
          bind:this={refs[i]}
          style="height: 100px; display: flex; align-items: center; flex-shrink: 0;"
        >
          {item.text}
        </div>
      {/each}
    </div>
  {/snippet}
</MultipleIntersectionObserver>

Same rule as the scroll-to-end example: root must scroll on its own. Here itemsContainer has an explicit height and overflow-y: auto.

Avoid putting the single-element IntersectionObserver inside an #each with one variable shared across iterations, such as let node; outside the loop and bind:this={node} inside. Every iteration overwrites node, so each observer keeps chasing a moving target and can loop forever. Use MultipleIntersectionObserver with a per-item ref instead.

Props

NameDescriptionTypeDefault value
elementsArray of elements to observeArray<null | undefined | Element>[]
rootContaining elementElement | Document | nullnull
rootMarginMargin offset of the containing elementstring"0px"
thresholdPercentage of element visibility to trigger an eventnumber between 0 and 1, or an array of numbers between 0 and 10
onceUnobserve each element after its first intersection eventbooleanfalse
skipPause observing all elements without clearing current statebooleanfalse
elementIntersectionsMap of each element to its current intersecting stateMap<null | undefined | Element, boolean>new Map()
elementEntriesMap of each element to its latest observer entryMap<null | undefined | Element, IntersectionObserverEntry>new Map()
observerShared IntersectionObserver instancenull or IntersectionObservernull

Callback props

Same onobserve/onintersect/onexit behavior as described in Callbacks above. Each callback receives the element's individual IntersectionObserverEntry.

children snippet props

NameType
elementIntersectionsMap<null | undefined | Element, boolean>
elementEntriesMap<null | undefined | Element, IntersectionObserverEntry>
observerIntersectionObserver

intersect

Use the intersect action to observe an element with use: and no wrapper component.

<script lang="ts">
  import { intersect } from "svelte-intersection-observer";

  let inView = $state(false);
</script>

<div
  use:intersect
  onobserve={(e) => {
    inView = e.detail.isIntersecting;
  }}
>
  {inView ? "In view" : "Not in view"}
</div>
<script>
  import { intersect } from "svelte-intersection-observer";

  let inView = $state(false);
</script>

<div
  use:intersect
  onobserve={(e) => {
    inView = e.detail.isIntersecting;
  }}
>
  {inView ? "In view" : "Not in view"}
</div>

Parameters

NameDescriptionTypeDefault value
onceUnobserve the element after the first intersection eventbooleanfalse
skipPause observing without losing entry/intersecting statebooleanfalse
rootContaining elementElement | Document | nullnull
rootMarginMargin offset of the containing elementstring"0px"
thresholdPercentage of element visibility to trigger an eventnumber between 0 and 1, or an array of numbers between 0 and 10
onobserveCalled when the element is first observed or when an intersection change occurs(entry: IntersectionObserverEntry) => voidundefined
onintersectCalled when the element is intersecting the viewport(entry: IntersectionObserverEntry) => voidundefined
onexitCalled when the element stops intersecting(entry: IntersectionObserverEntry) => voidundefined

intersectAttachment

Use intersectAttachment in Svelte 5.29+ if you prefer {@attach} over use:.

<script lang="ts">
  import { intersectAttachment } from "svelte-intersection-observer";

  let inView = $state(false);
</script>

<div
  {@attach intersectAttachment()}
  onobserve={(e) => {
    inView = e.detail.isIntersecting;
  }}
>
  {inView ? "In view" : "Not in view"}
</div>
<script>
  import { intersectAttachment } from "svelte-intersection-observer";

  let inView = $state(false);
</script>

<div
  {@attach intersectAttachment()}
  onobserve={(e) => {
    inView = e.detail.isIntersecting;
  }}
>
  {inView ? "In view" : "Not in view"}
</div>

Parameters

Same options and callbacks as intersect.

createIntersectionObserver

Use createIntersectionObserver to create and control observation from <script> without a directive or wrapper.

<script lang="ts">
  import { createIntersectionObserver } from "svelte-intersection-observer";

  let target: HTMLElement | undefined = $state();

  const observer = createIntersectionObserver(() => ({
    element: target,
    threshold: 0.5,
  }));
</script>

<div bind:this={target}>
  {observer.intersecting ? "Half visible" : "Less than half visible"}
</div>
<script>
  import { createIntersectionObserver } from "svelte-intersection-observer";

  let target = $state();

  const observer = createIntersectionObserver(() => ({
    element: target,
    threshold: 0.5,
  }));
</script>

<div bind:this={target}>
  {observer.intersecting ? "Half visible" : "Less than half visible"}
</div>

Signature

function createIntersectionObserver(
  getOptions?: () => IntersectOptions,
): {
  readonly intersecting: boolean;
  readonly entry: IntersectionObserverEntry | null;
  readonly observer: IntersectionObserver | null;
};

Options

Same core options as IntersectionObserver: element, root, rootMargin, threshold, once, and skip.

Callback options

Same onobserve/onintersect/onexit behavior as described in Callbacks above.

createIntersectionGroup

Use createIntersectionGroup for one shared observer across many elements, attaching to each node with {@attach}.

<script lang="ts">
  import { createIntersectionGroup } from "svelte-intersection-observer";

  let sections = [
    { id: "intro", title: "Intro", visible: false },
    { id: "details", title: "Details", visible: false },
    { id: "summary", title: "Summary", visible: false },
  ];

  let sectionRoot: HTMLElement | undefined = $state();

  const group = createIntersectionGroup(() => ({
    root: sectionRoot,
    threshold: 0.25,
  }));
</script>

<div bind:this={sectionRoot} style="height: 200px; overflow-y: auto;">
  {#each sections as section (section.id)}
    <section
      id={section.id}
      style="height: 160px;"
      {@attach group.attach({
        onobserve: (entry) => {
          section.visible = entry.isIntersecting;
        },
      })}
    >
      <h3>{section.title}</h3>
      <p>{section.visible ? "Visible" : "Not visible"}</p>
    </section>
  {/each}
</div>
<script>
  import { createIntersectionGroup } from "svelte-intersection-observer";

  let sections = [
    { id: "intro", title: "Intro", visible: false },
    { id: "details", title: "Details", visible: false },
    { id: "summary", title: "Summary", visible: false },
  ];

  let sectionRoot = $state();

  const group = createIntersectionGroup(() => ({
    root: sectionRoot,
    threshold: 0.25,
  }));
</script>

<div bind:this={sectionRoot} style="height: 200px; overflow-y: auto;">
  {#each sections as section (section.id)}
    <section
      id={section.id}
      style="height: 160px;"
      {@attach group.attach({
        onobserve: (entry) => {
          section.visible = entry.isIntersecting;
        },
      })}
    >
      <h3>{section.title}</h3>
      <p>{section.visible ? "Visible" : "Not visible"}</p>
    </section>
  {/each}
</div>

Signature

function createIntersectionGroup(
  getSharedOptions?: () => IntersectGroupSharedOptions,
): IntersectionGroup;

Shared options

Passed once to createIntersectionGroup. Apply to every element in the group.

NameDescriptionTypeDefault value
rootContaining elementnull or HTMLElementnull
rootMarginMargin offset of the containing elementstring"0px"
thresholdPercentage of element visibility to trigger an eventnumber between 0 and 1, or an array of numbers between 0 and 10

group.attach(options) per-node options

Passed once per element, to group.attach(...).

NameDescriptionTypeDefault value
onceUnobserve the element after the first intersection eventbooleanfalse
skipSkip observing this element without affecting the groupbooleanfalse
onobserveCalled when the element is first observed or when an intersection change occurs(entry: IntersectionObserverEntry) => voidundefined
onintersectCalled when the element is intersecting the viewport(entry: IntersectionObserverEntry) => voidundefined
onexitCalled when the element stops intersecting(entry: IntersectionObserverEntry) => voidundefined

SSR

All exports are SSR-safe. You do not need guards. On the server nothing is observed, so intersecting is false and entry is null. Observation starts after mount in the browser. If above-the-fold content must look correct in the server HTML, do not gate its critical rendering on intersecting.

Use cases

Concrete setups using the exports above.

Lazy-loading images

Delay the real src until the image is about to enter view. rootMargin starts the fetch a bit early so it is ready when the user scrolls to it. once stops observing after load.

<script lang="ts">
  import { intersectAttachment } from "svelte-intersection-observer";

  let loaded = $state(false);
</script>

<img
  {@attach intersectAttachment(() => ({ once: true, rootMargin: "200px" }))}
  onintersect={() => (loaded = true)}
  src={loaded ? "/photo.jpg" : "/placeholder.jpg"}
  alt=""
/>
<script>
  import { intersectAttachment } from "svelte-intersection-observer";

  let loaded = $state(false);
</script>

<img
  {@attach intersectAttachment(() => ({ once: true, rootMargin: "200px" }))}
  onintersect={() => (loaded = true)}
  src={loaded ? "/photo.jpg" : "/placeholder.jpg"}
  alt=""
/>

Autoplaying video

Play a <video> while it is on screen, pause when it leaves. This needs a reaction on every visibility change, so use onintersect/onexit and skip once.

<script lang="ts">
  import { intersect } from "svelte-intersection-observer";

  let video: HTMLVideoElement | undefined = $state();
</script>

<video
  bind:this={video}
  use:intersect
  onintersect={() => video?.play()}
  onexit={() => video?.pause()}
  src="/clip.mp4"
  muted
  loop
></video>
<script>
  import { intersect } from "svelte-intersection-observer";

  let video = $state();
</script>

<video
  bind:this={video}
  use:intersect
  onintersect={() => video?.play()}
  onexit={() => video?.pause()}
  src="/clip.mp4"
  muted
  loop
></video>

Tracking impressions

Fire an impression the first time an element is meaningfully visible. threshold defines "visible enough." once keeps it to a single event per element.

<div
  use:intersect={{ once: true, threshold: 0.5 }}
  onintersect={() => analytics.track("ad_impression", { id: "banner-1" })}
>
  <!-- ad content -->
</div>

Infinite scroll

To detect scroll-to-end in a scrollable container, put a sentinel after the content and set root to the container. intersecting becomes true when the sentinel enters view.

Note: root must be the scrollable element itself. It needs its own overflow and fixed height, not merely an ancestor that scrolls. If root only sits inside a scrollable ancestor, the sentinel moves with root and never changes position relative to it, so it stays permanently intersecting.

Keep scrolling...

Paragraph 1

Paragraph 2

Paragraph 3

Paragraph 4

Paragraph 5

Paragraph 6

Paragraph 7

Paragraph 8

Paragraph 9

Paragraph 10

Paragraph 11

Paragraph 12

Paragraph 13

Paragraph 14

Paragraph 15

Paragraph 16

Paragraph 17

Paragraph 18

Paragraph 19

Paragraph 20

<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";

  let container: HTMLElement | undefined = $state();
  let sentinel: HTMLElement | undefined = $state();
  let reachedEnd = $state(false);
</script>

<header class:intersecting={reachedEnd}>
  {reachedEnd ? "You've reached the end" : "Keep scrolling..."}
</header>

<div bind:this={container} style="height: 200px; overflow-y: auto;">
  {#each Array.from({ length: 20 }) as _, i}
    <p>Paragraph {i + 1}</p>
  {/each}

  <IntersectionObserver
    element={sentinel}
    root={container}
    bind:intersecting={reachedEnd}
  >
    <div bind:this={sentinel} style="height: 1px;"></div>
  </IntersectionObserver>
</div>
<script>
  import IntersectionObserver from "svelte-intersection-observer";

  let container = $state();
  let sentinel = $state();
  let reachedEnd = $state(false);
</script>

<header class:intersecting={reachedEnd}>
  {reachedEnd ? "You've reached the end" : "Keep scrolling..."}
</header>

<div bind:this={container} style="height: 200px; overflow-y: auto;">
  {#each Array.from({ length: 20 }) as _, i}
    <p>Paragraph {i + 1}</p>
  {/each}

  <IntersectionObserver
    element={sentinel}
    root={container}
    bind:intersecting={reachedEnd}
  >
    <div bind:this={sentinel} style="height: 1px;"></div>
  </IntersectionObserver>
</div>

Same sentinel pattern for infinite scroll: call loadMore() from onintersect instead of, or as well as, updating reachedEnd.

Reveal animation on scroll

Keep the bind:this element outside {#if intersecting}. Only gate the animated content inside. The bound element stays in the DOM before the reveal, so things like scrollIntoView() still work, and the animation can replay each time the element enters or leaves view.

Not in view

<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";
  import { fly } from "svelte/transition";

  let revealNode: HTMLElement | undefined = $state();
  let revealIntersecting = $state(false);
</script>

<header class:intersecting={revealIntersecting}>
  <p>{revealIntersecting ? "In view" : "Not in view"}</p>
  <button
    style="margin-top: 0.75rem;"
    onclick={() => {
      revealNode?.scrollIntoView({ behavior: "smooth", block: "nearest" });
    }}
  >
    Scroll to section
  </button>
</header>

<IntersectionObserver element={revealNode} bind:intersecting={revealIntersecting}>
  <div bind:this={revealNode}>
    {#if revealIntersecting}
      <section transition:fly={{ y: 50, duration: 300 }}>
        Hello world
      </section>
    {/if}
  </div>
</IntersectionObserver>
<script>
  import IntersectionObserver from "svelte-intersection-observer";
  import { fly } from "svelte/transition";

  let revealNode = $state();
  let revealIntersecting = $state(false);
</script>

<header class:intersecting={revealIntersecting}>
  <p>{revealIntersecting ? "In view" : "Not in view"}</p>
  <button
    style="margin-top: 0.75rem;"
    onclick={() => {
      revealNode?.scrollIntoView({ behavior: "smooth", block: "nearest" });
    }}
  >
    Scroll to section
  </button>
</header>

<IntersectionObserver element={revealNode} bind:intersecting={revealIntersecting}>
  <div bind:this={revealNode}>
    {#if revealIntersecting}
      <section transition:fly={{ y: 50, duration: 300 }}>
        Hello world
      </section>
    {/if}
  </div>
</IntersectionObserver>

Respect prefers-reduced-motion by gating the transition, for example with Svelte's prefersReducedMotion store from svelte/motion (≥5.7).

skip

Set skip to true to unobserve without disconnecting the observer or clearing entry/intersecting. Handy for pausing on an off-screen carousel panel or a closed modal. Set it back to false to resume. Unlike once, this toggles freely. MultipleIntersectionObserver and the intersect action support skip too.

<script lang="ts">
  import IntersectionObserver from "svelte-intersection-observer";

  let elementSkip: HTMLElement | undefined = $state();
  let paused = $state(false);
</script>

<button onclick={() => (paused = !paused)}>
  {paused ? "Resume" : "Pause"}
</button>

<IntersectionObserver element={elementSkip} skip={paused}>
  {#snippet children({ intersecting })}
    <div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
  {/snippet}
</IntersectionObserver>
<script>
  import IntersectionObserver from "svelte-intersection-observer";

  let elementSkip = $state();
  let paused = $state(false);
</script>

<button onclick={() => (paused = !paused)}>
  {paused ? "Resume" : "Pause"}
</button>

<IntersectionObserver element={elementSkip} skip={paused}>
  {#snippet children({ intersecting })}
    <div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
  {/snippet}
</IntersectionObserver>

List strategy

MultipleIntersectionObserver and createIntersectionGroup both share one observer across many elements. Choose by wiring style:

Either way, do not give IntersectionObserver a single shared bind:this inside #each. See the warning under pooled component.

IntersectionObserverEntry

Fields on an IntersectionObserverEntry are read-only. The ones you reach for most are isIntersecting, intersectionRatio, and target. MDN covers the rest.