Skip to content

v2.0.0

Choose a tag to compare

@metonym metonym released this 05 Jul 01:00
· 48 commits to master since this release
v2.0.0
d1b1c19

Svelte 5 Runes support

Breaking Changes

  • Port IntersectionObserver and MultipleIntersectionObserver to Svelte 5 runes; requires svelte@^5 in runes mode
  • Replace the default <slot> with a children snippet — use {#snippet children({ intersecting, entry, observer })} instead of let:intersecting/let:entry/let:observer (and let:elementIntersections/let:elementEntries/let:observer for MultipleIntersectionObserver)
  • Replace on:observe/on:intersect dispatched events with onobserve/onintersect callback props, called directly with the IntersectionObserverEntry (or { entry, target } for MultipleIntersectionObserver) instead of a CustomEvent
  • use:intersect now listens via onobserve/onintersect attributes instead of on:observe/on:intersect (the action itself still dispatches native CustomEvents)
  • Rename src/intersect.js/src/intersect.d.ts to src/intersect.svelte.js/src/intersect.svelte.d.ts

Migration guide

Svelte 5 runes mode is now required

v1 worked with Svelte 3, 4, and 5 (in legacy/non-runes mode). v2 only works with Svelte 5 in runes mode. If your project hasn't migrated to runes yet, stay on v1 until it has.

// package.json
{
  "dependencies": {
    // Before
    "svelte-intersection-observer": "^1.0.0",
    // After
    "svelte-intersection-observer": "^2.0.0",
  },
}

Slot props → children snippet

The default <slot> is now a children snippet. Replace let:xxx directives with a {#snippet children({ ... })} block.

<!-- Before (v1) -->
<IntersectionObserver {element} let:intersecting let:entry let:observer>
  <div bind:this={element}>
    {intersecting ? "In view" : "Not in view"}
  </div>
</IntersectionObserver>

<!-- After (v2) -->
<IntersectionObserver {element}>
  {#snippet children({ intersecting, entry, observer })}
    <div bind:this={element}>
      {intersecting ? "In view" : "Not in view"}
    </div>
  {/snippet}
</IntersectionObserver>

If you only bind to intersecting/entry/observer with bind: (rather than reading the slot props), you don't need the children snippet at all — plain child markup still works:

<!-- Still valid in v2 -->
<IntersectionObserver {element} bind:intersecting>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>

The same applies to MultipleIntersectionObserver, whose slot props were elementIntersections, elementEntries, and observer:

<!-- Before (v1) -->
<MultipleIntersectionObserver {elements} let:elementIntersections>
  <div bind:this={ref1}>{elementIntersections.get(ref1) ? "" : ""}</div>
</MultipleIntersectionObserver>

<!-- After (v2) -->
<MultipleIntersectionObserver {elements}>
  {#snippet children({ elementIntersections })}
    <div bind:this={ref1}>{elementIntersections.get(ref1) ? "" : ""}</div>
  {/snippet}
</MultipleIntersectionObserver>

on:observe/on:intersectonobserve/onintersect callback props

Component events are no longer dispatched via createEventDispatcher. Listen with callback props instead — and note the callback receives the IntersectionObserverEntry directly, not a CustomEvent wrapping it.

<!-- Before (v1) -->
<IntersectionObserver
  {element}
  on:observe={(e) => console.log(e.detail.isIntersecting)}
  on:intersect={(e) => console.log(e.detail)}
>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>

<!-- After (v2) -->
<IntersectionObserver
  {element}
  onobserve={(entry) => console.log(entry.isIntersecting)}
  onintersect={(entry) => console.log(entry)}
>
  <div bind:this={element}>Hello world</div>
</IntersectionObserver>

For MultipleIntersectionObserver, the callback receives { entry, target } directly instead of e.detail:

<!-- Before (v1) -->
<MultipleIntersectionObserver
  {elements}
  on:observe={(e) => console.log(e.detail.target, e.detail.entry)}
/>

<!-- After (v2) -->
<MultipleIntersectionObserver
  {elements}
  onobserve={({ target, entry }) => console.log(target, entry)}
/>

use:intersect action: on:observe/on:intersectonobserve/onintersect

The action itself is unchanged — it still dispatches native CustomEvents on the element — but the on: directive is gone in Svelte 5, so listen with the plain event attributes instead. e.detail is unchanged.

<!-- Before (v1) -->
<div
  use:intersect={{ once: true }}
  on:observe={(e) => (intersecting = e.detail.isIntersecting)}
>
  Hello world
</div>

<!-- After (v2) -->
<div
  use:intersect={{ once: true }}
  onobserve={(e) => (intersecting = e.detail.isIntersecting)}
>
  Hello world
</div>

Type imports

If you imported the action's types via a subpath (rather than the root package export), update the path to match the renamed file:

// Before (v1)
import type { IntersectActionOptions } from "svelte-intersection-observer/intersect";

// After (v2)
import type { IntersectActionOptions } from "svelte-intersection-observer/intersect.svelte";

The root package export (import { intersect } from "svelte-intersection-observer") is unaffected.