svelte-intersection-observer 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IntersectionObserver.svelte +27 -23
- package/MultipleIntersectionObserver.svelte +47 -40
- package/README.md +428 -233
- package/create-intersection-observer.svelte.d.ts +22 -0
- package/create-intersection-observer.svelte.js +36 -0
- package/index.d.ts +13 -2
- package/index.js +6 -1
- package/intersect.svelte.d.ts +90 -0
- package/intersect.svelte.js +76 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
# svelte-intersection-observer
|
|
2
2
|
|
|
3
|
+
<!-- HIDE_START -->
|
|
3
4
|
[![NPM][npm]][npm-url]
|
|
5
|
+
<!-- HIDE_END -->
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## About
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
`svelte-intersection-observer` is a zero-dependency Svelte library built on the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) that detects when an element enters or exits the viewport, without expensive scroll listeners. Use it for lazy-loading, scroll animations, infinite scroll, autoplaying video, impression tracking, and more (see [Use Cases](#use-cases)).
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
It offers six interchangeable primitives, all backed by the same shared observer logic.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
| Primitive | Export | Use it when... |
|
|
14
|
+
| :-------- | :----- | :--------------------- |
|
|
15
|
+
| [Component](#intersectionobserver) | `IntersectionObserver` | you want a component with a bound `intersecting` prop |
|
|
16
|
+
| [Pooled component](#multipleintersectionobserver) | `MultipleIntersectionObserver` | you're observing many elements and want one shared observer |
|
|
17
|
+
| [Action](#intersect) | `intersect` | you want `use:` on a plain element, no extra markup |
|
|
18
|
+
| [Attachment](#intersectattachment) | `intersectAttachment` | same as the action, composed via `{@attach}` (Svelte 5.29+) |
|
|
19
|
+
| [Composable](#createintersectionobserver) | `createIntersectionObserver` | you want reactive state from `<script>`, no directive |
|
|
20
|
+
| [Factory](#createintersectiongroup) | `createIntersectionGroup` | you're rendering a list with `{@attach}` and want one shared observer |
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
See [Library](#library) for the full docs on each. Try it in the [Svelte REPL](https://svelte.dev/repl/8cd2327a580c4f429c71f7df999bd51d).
|
|
23
|
+
|
|
24
|
+
### Compatibility
|
|
14
25
|
|
|
15
26
|
| Package version | Svelte version | Notes |
|
|
16
27
|
| :--------------- | :----------------- | :----------------------------------------- |
|
|
17
|
-
| 1.x
|
|
28
|
+
| [1.x](https://github.com/metonym/svelte-intersection-observer/tree/v1.2.x) | 3, 4, 5 (non-runes) | Uses `export let`, slots, and `on:` events |
|
|
18
29
|
| 2.x | 5 (runes mode only) | Uses `$props()`, snippets, and callback props |
|
|
19
30
|
|
|
20
31
|
<!-- TOC -->
|
|
@@ -36,19 +47,21 @@ yarn add svelte-intersection-observer
|
|
|
36
47
|
|
|
37
48
|
```
|
|
38
49
|
|
|
39
|
-
##
|
|
50
|
+
## Library
|
|
51
|
+
|
|
52
|
+
Every primitive shares the same core options (`root`, `rootMargin`, `threshold`, `once`, `skip`) and the same `onobserve`/`onintersect` callbacks. Only how you plug it into your markup differs. See [Use Cases](#use-cases) for realistic scenarios built from these.
|
|
40
53
|
|
|
41
|
-
###
|
|
54
|
+
### `IntersectionObserver`
|
|
42
55
|
|
|
43
56
|
Use the [`bind:this`](https://svelte.dev/docs#bind_element) directive to pass an element reference to the `IntersectionObserver` component.
|
|
44
57
|
|
|
45
|
-
Then
|
|
58
|
+
Then bind to the reactive `intersecting` prop to check whether the element intersects the viewport.
|
|
46
59
|
|
|
47
60
|
```svelte
|
|
48
|
-
<script>
|
|
61
|
+
<script lang="ts">
|
|
49
62
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
50
63
|
|
|
51
|
-
let element = $state();
|
|
64
|
+
let element: HTMLElement | undefined = $state();
|
|
52
65
|
let intersecting = $state(false);
|
|
53
66
|
</script>
|
|
54
67
|
|
|
@@ -61,42 +74,15 @@ Then, simply bind to the reactive `intersecting` prop to determine if the elemen
|
|
|
61
74
|
</IntersectionObserver>
|
|
62
75
|
```
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
An alternative to binding to the `intersecting` prop is to use the `children` snippet, which receives `intersecting`, `entry`, and `observer`.
|
|
67
|
-
|
|
68
|
-
In the following example, the "Hello world" element fades in when its containing element intersects the viewport.
|
|
69
|
-
|
|
70
|
-
```svelte
|
|
71
|
-
<script>
|
|
72
|
-
import IntersectionObserver from "svelte-intersection-observer";
|
|
73
|
-
import { fade } from "svelte/transition";
|
|
74
|
-
|
|
75
|
-
let node = $state();
|
|
76
|
-
</script>
|
|
77
|
-
|
|
78
|
-
<header></header>
|
|
79
|
-
|
|
80
|
-
<IntersectionObserver element={node}>
|
|
81
|
-
{#snippet children({ intersecting })}
|
|
82
|
-
<div bind:this={node}>
|
|
83
|
-
{#if intersecting}
|
|
84
|
-
<div transition:fade={{ delay: 200 }}>Hello world</div>
|
|
85
|
-
{/if}
|
|
86
|
-
</div>
|
|
87
|
-
{/snippet}
|
|
88
|
-
</IntersectionObserver>
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
### Once
|
|
77
|
+
#### `once`
|
|
92
78
|
|
|
93
|
-
Set `once` to `true`
|
|
79
|
+
Set `once` to `true` to unobserve the element after its first intersection event, useful for a one-time reveal animation, a single lazy-load, or a single analytics impression.
|
|
94
80
|
|
|
95
81
|
```svelte
|
|
96
|
-
<script>
|
|
82
|
+
<script lang="ts">
|
|
97
83
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
98
84
|
|
|
99
|
-
let elementOnce = $state();
|
|
85
|
+
let elementOnce: HTMLElement | undefined = $state();
|
|
100
86
|
let intersectOnce = $state(false);
|
|
101
87
|
</script>
|
|
102
88
|
|
|
@@ -113,167 +99,71 @@ Set `once` to `true` for the intersection event to occur only once. The `element
|
|
|
113
99
|
</IntersectionObserver>
|
|
114
100
|
```
|
|
115
101
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
Set `skip` to `true` to unobserve without disconnecting the underlying observer or losing `entry`/`intersecting` state. This is useful for pausing tracking on an off-screen carousel panel or a closed modal. Set `skip` back to `false` to resume; unlike `once`, this can be toggled back and forth. `MultipleIntersectionObserver` and the `intersect` action support the same `skip` option.
|
|
119
|
-
|
|
120
|
-
```svelte no-eval
|
|
121
|
-
<script>
|
|
122
|
-
import IntersectionObserver from "svelte-intersection-observer";
|
|
123
|
-
|
|
124
|
-
let elementSkip = $state();
|
|
125
|
-
let paused = $state(false);
|
|
126
|
-
</script>
|
|
127
|
-
|
|
128
|
-
<button onclick={() => (paused = !paused)}>
|
|
129
|
-
{paused ? "Resume" : "Pause"}
|
|
130
|
-
</button>
|
|
131
|
-
|
|
132
|
-
<IntersectionObserver element={elementSkip} skip={paused}>
|
|
133
|
-
{#snippet children({ intersecting })}
|
|
134
|
-
<div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
|
|
135
|
-
{/snippet}
|
|
136
|
-
</IntersectionObserver>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### `onobserve` callback prop
|
|
140
|
-
|
|
141
|
-
`onobserve` is called when the element is first observed and also whenever an intersection event occurs.
|
|
142
|
-
|
|
143
|
-
```svelte no-eval
|
|
144
|
-
<IntersectionObserver
|
|
145
|
-
{element}
|
|
146
|
-
onobserve={(entry) => {
|
|
147
|
-
console.log(entry); // IntersectionObserverEntry
|
|
148
|
-
console.log(entry.isIntersecting); // true | false
|
|
149
|
-
}}
|
|
150
|
-
>
|
|
151
|
-
<div bind:this={element}>Hello world</div>
|
|
152
|
-
</IntersectionObserver>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### `onintersect` callback prop
|
|
156
|
-
|
|
157
|
-
As an alternative to binding the `intersecting` prop, you can pass an `onintersect` callback that is called if the observed element is intersecting the viewport.
|
|
158
|
-
|
|
159
|
-
**Note**: Compared to `onobserve`, `onintersect` is called only when the element is _intersecting the viewport_. In other words, `entry.isIntersecting` will only be `true`.
|
|
160
|
-
|
|
161
|
-
```svelte no-eval
|
|
162
|
-
<IntersectionObserver
|
|
163
|
-
{element}
|
|
164
|
-
onintersect={(entry) => {
|
|
165
|
-
console.log(entry); // IntersectionObserverEntry
|
|
166
|
-
console.log(entry.isIntersecting); // true
|
|
167
|
-
}}
|
|
168
|
-
>
|
|
169
|
-
<div bind:this={element}>Hello world</div>
|
|
170
|
-
</IntersectionObserver>
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### Detecting scroll to end
|
|
174
|
-
|
|
175
|
-
To detect when a user has scrolled to the end of a scrollable container, place a sentinel element after the content and set `root` to the container. `intersecting` becomes `true` once the sentinel scrolls into view.
|
|
176
|
-
|
|
177
|
-
**Note**: `root` must be the scrollable element itself (i.e. it has its own `overflow`/fixed `height`), not just an ancestor of one. If `root` merely sits inside a scrollable ancestor, the sentinel scrolls along with `root` and never changes position relative to it, so it reports as permanently intersecting.
|
|
178
|
-
|
|
179
|
-
```svelte
|
|
180
|
-
<script>
|
|
181
|
-
import IntersectionObserver from "svelte-intersection-observer";
|
|
182
|
-
|
|
183
|
-
let container = $state();
|
|
184
|
-
let sentinel = $state();
|
|
185
|
-
let reachedEnd = $state(false);
|
|
186
|
-
</script>
|
|
187
|
-
|
|
188
|
-
<header class:intersecting={reachedEnd}>
|
|
189
|
-
{reachedEnd ? "You've reached the end" : "Keep scrolling..."}
|
|
190
|
-
</header>
|
|
191
|
-
|
|
192
|
-
<div bind:this={container} style="height: 200px; overflow-y: auto;">
|
|
193
|
-
{#each Array.from({ length: 20 }) as _, i}
|
|
194
|
-
<p>Paragraph {i + 1}</p>
|
|
195
|
-
{/each}
|
|
196
|
-
|
|
197
|
-
<IntersectionObserver
|
|
198
|
-
element={sentinel}
|
|
199
|
-
root={container}
|
|
200
|
-
bind:intersecting={reachedEnd}
|
|
201
|
-
>
|
|
202
|
-
<div bind:this={sentinel} style="height: 1px;"></div>
|
|
203
|
-
</IntersectionObserver>
|
|
204
|
-
</div>
|
|
205
|
-
```
|
|
102
|
+
#### `children` snippet
|
|
206
103
|
|
|
207
|
-
|
|
104
|
+
An alternative to binding to the `intersecting` prop is to use the `children` snippet, which receives `intersecting`, `entry`, and `observer`.
|
|
208
105
|
|
|
209
|
-
|
|
106
|
+
In this example, "Hello world" fades in when its containing element intersects the viewport.
|
|
210
107
|
|
|
211
108
|
```svelte
|
|
212
|
-
<script>
|
|
109
|
+
<script lang="ts">
|
|
213
110
|
import IntersectionObserver from "svelte-intersection-observer";
|
|
214
|
-
import {
|
|
111
|
+
import { fade } from "svelte/transition";
|
|
215
112
|
|
|
216
|
-
let
|
|
217
|
-
let revealIntersecting = $state(false);
|
|
113
|
+
let node: HTMLElement | undefined = $state();
|
|
218
114
|
</script>
|
|
219
115
|
|
|
220
|
-
<header
|
|
221
|
-
<button
|
|
222
|
-
onclick={() => {
|
|
223
|
-
revealNode.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
224
|
-
}}
|
|
225
|
-
>
|
|
226
|
-
Scroll to section
|
|
227
|
-
</button>
|
|
228
|
-
</header>
|
|
116
|
+
<header></header>
|
|
229
117
|
|
|
230
|
-
<IntersectionObserver element={
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
Hello world
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
118
|
+
<IntersectionObserver element={node}>
|
|
119
|
+
{#snippet children({ intersecting })}
|
|
120
|
+
<div bind:this={node}>
|
|
121
|
+
{#if intersecting}
|
|
122
|
+
<div transition:fade={{ delay: 200 }}>Hello world</div>
|
|
123
|
+
{/if}
|
|
124
|
+
</div>
|
|
125
|
+
{/snippet}
|
|
238
126
|
</IntersectionObserver>
|
|
239
127
|
```
|
|
240
128
|
|
|
241
|
-
|
|
129
|
+
#### Props
|
|
242
130
|
|
|
243
|
-
|
|
131
|
+
| Name | Description | Type | Default value |
|
|
132
|
+
| :----------- | :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------ |
|
|
133
|
+
| element | Observed element | `null` or `HTMLElement` | `null` |
|
|
134
|
+
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
|
|
135
|
+
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` | `false` |
|
|
136
|
+
| root | Containing element | `null` or `HTMLElement` | `null` |
|
|
137
|
+
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
|
|
138
|
+
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
|
|
139
|
+
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) | `null` |
|
|
140
|
+
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
141
|
+
| skip | Pause observing without losing `entry`/`intersecting` state | `boolean` | `false` |
|
|
244
142
|
|
|
245
|
-
|
|
246
|
-
<script>
|
|
247
|
-
import { intersect } from "svelte-intersection-observer";
|
|
143
|
+
**Note**: the observed `element` must render with a non-zero width and height for `threshold` values greater than `0` to have any effect. This is a constraint of the underlying [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), not something this component controls.
|
|
248
144
|
|
|
249
|
-
|
|
250
|
-
</script>
|
|
145
|
+
#### Callback props
|
|
251
146
|
|
|
252
|
-
|
|
253
|
-
{actionIntersecting ? "Element is in view" : "Element is not in view"}
|
|
254
|
-
</header>
|
|
147
|
+
Same `onobserve`/`onintersect` behavior as described in [Callbacks](#callbacks-onobserve-and-onintersect) below.
|
|
255
148
|
|
|
256
|
-
|
|
257
|
-
use:intersect={{ once: true }}
|
|
258
|
-
onobserve={(e) => (actionIntersecting = e.detail.isIntersecting)}
|
|
259
|
-
>
|
|
260
|
-
Hello world
|
|
261
|
-
</div>
|
|
262
|
-
```
|
|
149
|
+
#### `children` snippet props
|
|
263
150
|
|
|
264
|
-
|
|
151
|
+
| Name | Type |
|
|
152
|
+
| :----------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
153
|
+
| intersecting | `boolean` |
|
|
154
|
+
| entry | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
|
|
155
|
+
| observer | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
|
|
265
156
|
|
|
266
|
-
###
|
|
157
|
+
### `MultipleIntersectionObserver`
|
|
267
158
|
|
|
268
|
-
For performance, use `MultipleIntersectionObserver` to observe multiple elements with
|
|
159
|
+
For performance, use `MultipleIntersectionObserver` to observe multiple elements with one shared observer instead of instantiating one per element.
|
|
269
160
|
|
|
270
161
|
```svelte
|
|
271
|
-
<script>
|
|
162
|
+
<script lang="ts">
|
|
272
163
|
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
273
164
|
|
|
274
|
-
let ref1 = $state();
|
|
275
|
-
let ref2 = $state();
|
|
276
|
-
|
|
165
|
+
let ref1: HTMLElement | undefined = $state();
|
|
166
|
+
let ref2: HTMLElement | undefined = $state();
|
|
277
167
|
let elements = $derived([ref1, ref2]);
|
|
278
168
|
</script>
|
|
279
169
|
|
|
@@ -294,22 +184,21 @@ For performance, use `MultipleIntersectionObserver` to observe multiple elements
|
|
|
294
184
|
</MultipleIntersectionObserver>
|
|
295
185
|
```
|
|
296
186
|
|
|
297
|
-
|
|
187
|
+
#### Using with `#each`
|
|
298
188
|
|
|
299
189
|
`MultipleIntersectionObserver` also handles a dynamic, `#each`-driven list: give every item its own slot in an array/object instead of one shared variable.
|
|
300
190
|
|
|
301
191
|
```svelte
|
|
302
|
-
<script>
|
|
192
|
+
<script lang="ts">
|
|
303
193
|
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
|
|
304
194
|
|
|
305
|
-
let items = Array.from({ length:
|
|
195
|
+
let items = Array.from({ length: 5 }, (_, i) => ({
|
|
306
196
|
id: i + 1,
|
|
307
197
|
text: `Item ${i + 1}`,
|
|
308
198
|
}));
|
|
309
199
|
|
|
310
|
-
let refs = $state([]);
|
|
311
|
-
let itemsContainer = $state();
|
|
312
|
-
|
|
200
|
+
let refs: (HTMLElement | undefined)[] = $state([]);
|
|
201
|
+
let itemsContainer: HTMLElement | undefined = $state();
|
|
313
202
|
let itemElements = $derived(refs);
|
|
314
203
|
</script>
|
|
315
204
|
|
|
@@ -342,49 +231,12 @@ For performance, use `MultipleIntersectionObserver` to observe multiple elements
|
|
|
342
231
|
|
|
343
232
|
As with the scroll-to-end example, `root` must be an element that scrolls on its own; here, `itemsContainer` has an explicit `height` and `overflow-y: auto`.
|
|
344
233
|
|
|
345
|
-
**Avoid** using the single-element `IntersectionObserver` component inside an `#each` block with one variable shared across iterations (e.g. `let node;` declared outside the loop
|
|
346
|
-
|
|
347
|
-
## API
|
|
348
|
-
|
|
349
|
-
### IntersectionObserver
|
|
350
|
-
|
|
351
|
-
#### Props
|
|
352
|
-
|
|
353
|
-
| Name | Description | Type | Default value |
|
|
354
|
-
| :----------- | :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------ |
|
|
355
|
-
| element | Observed element | `null` or `HTMLElement` | `null` |
|
|
356
|
-
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
|
|
357
|
-
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` | `false` |
|
|
358
|
-
| root | Containing element | `null` or `HTMLElement` | `null` |
|
|
359
|
-
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
|
|
360
|
-
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
|
|
361
|
-
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) | `null` |
|
|
362
|
-
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
|
|
363
|
-
| skip | Pause observing without losing `entry`/`intersecting` state | `boolean` | `false` |
|
|
364
|
-
|
|
365
|
-
**Note**: the observed `element` must render with a non-zero width and height for `threshold` values greater than `0` to have any effect. This is a constraint of the underlying [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), not something this component controls.
|
|
366
|
-
|
|
367
|
-
#### Callback props
|
|
368
|
-
|
|
369
|
-
- **onobserve**: called when the element is first observed or whenever an intersection change occurs
|
|
370
|
-
- **onintersect**: called when the element is intersecting the viewport
|
|
371
|
-
|
|
372
|
-
Both callbacks are called with an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
|
|
373
|
-
|
|
374
|
-
#### `children` snippet props
|
|
375
|
-
|
|
376
|
-
| Name | Type |
|
|
377
|
-
| :----------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
378
|
-
| intersecting | `boolean` |
|
|
379
|
-
| entry | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
|
|
380
|
-
| observer | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
|
|
381
|
-
|
|
382
|
-
### MultipleIntersectionObserver
|
|
234
|
+
**Avoid** using the single-element `IntersectionObserver` component inside an `#each` block with one variable shared across iterations (e.g. `let node;` declared outside the loop, bound via `bind:this={node}` inside it). Every iteration overwrites the same `node`, so each observer keeps re-observing a moving target, which can cause an infinite update loop. Use `MultipleIntersectionObserver` with a per-item ref instead.
|
|
383
235
|
|
|
384
236
|
#### Props
|
|
385
237
|
|
|
386
238
|
| Name | Description | Type | Default value |
|
|
387
|
-
| :------------------- | :---------------------------------------------------- |
|
|
239
|
+
| :------------------- | :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
|
|
388
240
|
| elements | Array of HTML elements to observe | `Array<HTMLElement \| null>` | `[]` |
|
|
389
241
|
| once | Unobserve elements after the first intersection event | `boolean` | `false` |
|
|
390
242
|
| root | Containing element | `null` or `HTMLElement` | `null` |
|
|
@@ -397,10 +249,7 @@ Both callbacks are called with an [`IntersectionObserverEntry`](https://develope
|
|
|
397
249
|
|
|
398
250
|
#### Callback props
|
|
399
251
|
|
|
400
|
-
|
|
401
|
-
- **onintersect**: called when an element is intersecting the viewport
|
|
402
|
-
|
|
403
|
-
Both callbacks are called with:
|
|
252
|
+
Called with:
|
|
404
253
|
|
|
405
254
|
```ts
|
|
406
255
|
{
|
|
@@ -409,6 +258,8 @@ Both callbacks are called with:
|
|
|
409
258
|
}
|
|
410
259
|
```
|
|
411
260
|
|
|
261
|
+
See [Callbacks](#callbacks-onobserve-and-onintersect) for when each one fires.
|
|
262
|
+
|
|
412
263
|
#### `children` snippet props
|
|
413
264
|
|
|
414
265
|
| Name | Type |
|
|
@@ -417,7 +268,32 @@ Both callbacks are called with:
|
|
|
417
268
|
| elementIntersections | `Map<HTMLElement \| null, boolean>` |
|
|
418
269
|
| elementEntries | `Map<HTMLElement \| null,` [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)`>` |
|
|
419
270
|
|
|
420
|
-
### `intersect`
|
|
271
|
+
### `intersect`
|
|
272
|
+
|
|
273
|
+
As an alternative to the `IntersectionObserver` component, use the `intersect` action to observe an element directly with `use:`, without a `bind:this` reference or extra markup. Listen for `onobserve`/`onintersect` on the observed element itself.
|
|
274
|
+
|
|
275
|
+
```svelte
|
|
276
|
+
<script lang="ts">
|
|
277
|
+
import { intersect } from "svelte-intersection-observer";
|
|
278
|
+
|
|
279
|
+
let actionIntersecting = $state(false);
|
|
280
|
+
</script>
|
|
281
|
+
|
|
282
|
+
<header class:intersecting={actionIntersecting}>
|
|
283
|
+
{actionIntersecting ? "Element is in view" : "Element is not in view"}
|
|
284
|
+
</header>
|
|
285
|
+
|
|
286
|
+
<div
|
|
287
|
+
use:intersect={{ once: true }}
|
|
288
|
+
onobserve={(e) => {
|
|
289
|
+
actionIntersecting = e.detail.isIntersecting;
|
|
290
|
+
}}
|
|
291
|
+
>
|
|
292
|
+
Hello world
|
|
293
|
+
</div>
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Options passed to `use:intersect` are reactive: updating `root`, `rootMargin`, or `threshold` re-initializes the underlying observer. Updating `skip` toggles observing on the existing observer without re-initializing it.
|
|
421
297
|
|
|
422
298
|
#### Options
|
|
423
299
|
|
|
@@ -431,12 +307,331 @@ Both callbacks are called with:
|
|
|
431
307
|
|
|
432
308
|
#### Dispatched events
|
|
433
309
|
|
|
434
|
-
|
|
435
|
-
|
|
310
|
+
Same `onobserve`/`onintersect` behavior as described in [Callbacks](#callbacks-onobserve-and-onintersect); the action dispatches them on the element, and `e.detail` is the [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry).
|
|
311
|
+
|
|
312
|
+
### `intersectAttachment`
|
|
313
|
+
|
|
314
|
+
As of Svelte 5.29, [attachments](https://svelte.dev/docs/svelte/svelte-attachments) are the preferred replacement for actions. `intersectAttachment` wraps the `intersect` action with `svelte/attachments`'s `fromAction`, reusing the same observer logic but plugging into `{@attach ...}` instead of `use:`.
|
|
315
|
+
|
|
316
|
+
Attachments have a few architectural advantages over actions:
|
|
317
|
+
|
|
318
|
+
- No separate `update()` lifecycle method; they rerun reactively like a `$effect`
|
|
319
|
+
- Just plain functions, so they're easier to compose and generate dynamically
|
|
320
|
+
- Can be forwarded through components as ordinary props, unlike actions
|
|
321
|
+
|
|
322
|
+
```svelte
|
|
323
|
+
<script lang="ts">
|
|
324
|
+
import { intersectAttachment } from "svelte-intersection-observer";
|
|
325
|
+
|
|
326
|
+
let attachmentIntersecting = $state(false);
|
|
327
|
+
</script>
|
|
328
|
+
|
|
329
|
+
<header class:intersecting={attachmentIntersecting}>
|
|
330
|
+
{attachmentIntersecting ? "Element is in view" : "Element is not in view"}
|
|
331
|
+
</header>
|
|
332
|
+
|
|
333
|
+
<div
|
|
334
|
+
{@attach intersectAttachment(() => ({ once: true }))}
|
|
335
|
+
onobserve={(e) => {
|
|
336
|
+
attachmentIntersecting = e.detail.isIntersecting;
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
Hello world
|
|
340
|
+
</div>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Note**: unlike `use:intersect`, which takes the options object directly, `intersectAttachment` takes a function that _returns_ the options object (this is how `fromAction` tracks reactive dependencies). `intersect` remains fully supported; use whichever fits your codebase.
|
|
344
|
+
|
|
345
|
+
Options and dispatched events are identical to the [`intersect` action](#intersect) above.
|
|
346
|
+
|
|
347
|
+
### `createIntersectionObserver`
|
|
348
|
+
|
|
349
|
+
To get intersection state without wrapping markup in a component, use `createIntersectionObserver`, a script-only rune-based composable: call it in `<script>` for reactive `intersecting`/`entry` getters, then apply `attach` to the node with `{@attach}`.
|
|
350
|
+
|
|
351
|
+
```svelte no-eval
|
|
352
|
+
<script lang="ts">
|
|
353
|
+
import { createIntersectionObserver } from "svelte-intersection-observer";
|
|
354
|
+
|
|
355
|
+
const observer = createIntersectionObserver(() => ({ threshold: 0.5 }));
|
|
356
|
+
</script>
|
|
357
|
+
|
|
358
|
+
<div {@attach observer.attach}>
|
|
359
|
+
{observer.intersecting ? "In view" : "Not in view"}
|
|
360
|
+
</div>
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`createIntersectionObserver` takes the same options as [`intersectAttachment`](#intersectattachment) (as a function returning the options object) and reuses its underlying observer logic.
|
|
364
|
+
|
|
365
|
+
#### Return value
|
|
366
|
+
|
|
367
|
+
| Name | Description | Type |
|
|
368
|
+
| :----------- | :------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ |
|
|
369
|
+
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` |
|
|
370
|
+
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
|
|
371
|
+
| attach | Attachment to apply to the observed element via `{@attach}` | [`Attachment<HTMLElement>`](https://svelte.dev/docs/svelte/svelte-attachments) |
|
|
372
|
+
|
|
373
|
+
### `createIntersectionGroup`
|
|
374
|
+
|
|
375
|
+
A bare `intersect`/`intersectAttachment` inside an `#each` block creates one native `IntersectionObserver` per iteration: for a long list, that's N observers instead of 1. `createIntersectionGroup` fixes this for the action/attachment API: call it once to create a group, then call `group.attach(...)` once per element to get an attachment that shares a single underlying observer across the whole group.
|
|
376
|
+
|
|
377
|
+
```svelte
|
|
378
|
+
<script lang="ts">
|
|
379
|
+
import { createIntersectionGroup } from "svelte-intersection-observer";
|
|
380
|
+
|
|
381
|
+
let groupItems = $state(
|
|
382
|
+
Array.from({ length: 5 }, (_, i) => ({ id: i, intersecting: false })),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const group = createIntersectionGroup();
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<header>
|
|
389
|
+
{#each groupItems as item (item.id)}
|
|
390
|
+
<div class:intersecting={item.intersecting}>
|
|
391
|
+
Item {item.id}: {item.intersecting ? "✓" : "✗"}
|
|
392
|
+
</div>
|
|
393
|
+
{/each}
|
|
394
|
+
</header>
|
|
395
|
+
|
|
396
|
+
{#each groupItems as item (item.id)}
|
|
397
|
+
<div
|
|
398
|
+
class:intersecting={item.intersecting}
|
|
399
|
+
{@attach group.attach({
|
|
400
|
+
onobserve: (entry) => (item.intersecting = entry.isIntersecting),
|
|
401
|
+
})}
|
|
402
|
+
>
|
|
403
|
+
Item {item.id}
|
|
404
|
+
</div>
|
|
405
|
+
{/each}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
`root`/`rootMargin`/`threshold` configure the one shared observer, so they're passed once to `createIntersectionGroup` itself (as a function, for the same reactive-dependency-tracking reason `intersectAttachment` takes one) rather than per element:
|
|
409
|
+
|
|
410
|
+
```js
|
|
411
|
+
const group = createIntersectionGroup(() => ({
|
|
412
|
+
root: container,
|
|
413
|
+
rootMargin: "0px",
|
|
414
|
+
threshold: 0.5,
|
|
415
|
+
}));
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`once`, `skip`, `onobserve`, and `onintersect` are the only options that make sense per element, so those are what `group.attach(...)` accepts.
|
|
419
|
+
|
|
420
|
+
#### Signature
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
function createIntersectionGroup(
|
|
424
|
+
getSharedOptions?: () => IntersectGroupSharedOptions,
|
|
425
|
+
): IntersectionGroup;
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### Shared options
|
|
429
|
+
|
|
430
|
+
Passed once to `createIntersectionGroup`; apply to every element in the group.
|
|
431
|
+
|
|
432
|
+
| Name | Description | Type | Default value |
|
|
433
|
+
| :--------- | :----------------------------------------------------- | :----------------------------------------------------------------- | :------------ |
|
|
434
|
+
| root | Containing element | `null` or `HTMLElement` | `null` |
|
|
435
|
+
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
|
|
436
|
+
| threshold | Percentage of element visibility to trigger an event | `number` between 0 and 1, or an array of `number`s between 0 and 1 | `0` |
|
|
437
|
+
|
|
438
|
+
#### `group.attach(options)` per-node options
|
|
439
|
+
|
|
440
|
+
Passed once per element, to `group.attach(...)`.
|
|
441
|
+
|
|
442
|
+
| Name | Description | Type | Default value |
|
|
443
|
+
| :--------- | :---------------------------------------------------------- | :------------------------------------------------------- | :------------ |
|
|
444
|
+
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
|
|
445
|
+
| skip | Skip observing this element without affecting the group | `boolean` | `false` |
|
|
446
|
+
| onobserve | Called when the element is first observed or when an intersection change occurs | `(entry: IntersectionObserverEntry) => void` | `undefined` |
|
|
447
|
+
| onintersect | Called when the element is intersecting the viewport | `(entry: IntersectionObserverEntry) => void` | `undefined` |
|
|
448
|
+
|
|
449
|
+
#### Callbacks: `onobserve` and `onintersect`
|
|
450
|
+
|
|
451
|
+
Every primitive above exposes the same two callbacks, called with an [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) (components pass it directly; action and attachment dispatch it as `event.detail`):
|
|
452
|
+
|
|
453
|
+
- **onobserve**: called when the element is first observed, and again on every intersection change
|
|
454
|
+
- **onintersect**: called only when the element is intersecting the viewport (a filtered view of `onobserve`)
|
|
455
|
+
|
|
456
|
+
```svelte no-eval
|
|
457
|
+
<IntersectionObserver
|
|
458
|
+
{element}
|
|
459
|
+
onobserve={(entry) => {
|
|
460
|
+
console.log(entry); // IntersectionObserverEntry
|
|
461
|
+
console.log(entry.isIntersecting); // true | false
|
|
462
|
+
}}
|
|
463
|
+
onintersect={(entry) => {
|
|
464
|
+
console.log(entry.isIntersecting); // always true
|
|
465
|
+
}}
|
|
466
|
+
>
|
|
467
|
+
<div bind:this={element}>Hello world</div>
|
|
468
|
+
</IntersectionObserver>
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
## Use Cases
|
|
472
|
+
|
|
473
|
+
Realistic scenarios built from the primitives above.
|
|
474
|
+
|
|
475
|
+
### Lazy-loading images
|
|
476
|
+
|
|
477
|
+
Delay loading an image's real `src` until it's about to scroll into view. `rootMargin` starts the fetch slightly before the image is visible so it's ready when the user scrolls to it; `once` stops observing once it has loaded.
|
|
478
|
+
|
|
479
|
+
```svelte no-eval
|
|
480
|
+
<script lang="ts">
|
|
481
|
+
import { intersectAttachment } from "svelte-intersection-observer";
|
|
482
|
+
|
|
483
|
+
let loaded = $state(false);
|
|
484
|
+
</script>
|
|
485
|
+
|
|
486
|
+
<img
|
|
487
|
+
{@attach intersectAttachment(() => ({ once: true, rootMargin: "200px" }))}
|
|
488
|
+
onintersect={() => (loaded = true)}
|
|
489
|
+
src={loaded ? "/photo.jpg" : "/placeholder.jpg"}
|
|
490
|
+
alt=""
|
|
491
|
+
/>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Autoplaying video
|
|
495
|
+
|
|
496
|
+
Play a `<video>` while it's on screen and pause it once it scrolls away. Unlike lazy-loading, this needs to react every time visibility changes, so use `onobserve` (not `onintersect`) and skip `once`.
|
|
497
|
+
|
|
498
|
+
```svelte no-eval
|
|
499
|
+
<script lang="ts">
|
|
500
|
+
import { intersect } from "svelte-intersection-observer";
|
|
501
|
+
|
|
502
|
+
let video: HTMLVideoElement | undefined = $state();
|
|
503
|
+
</script>
|
|
504
|
+
|
|
505
|
+
<video
|
|
506
|
+
bind:this={video}
|
|
507
|
+
use:intersect
|
|
508
|
+
onobserve={(e) => {
|
|
509
|
+
e.detail.isIntersecting ? video?.play() : video?.pause();
|
|
510
|
+
}}
|
|
511
|
+
src="/clip.mp4"
|
|
512
|
+
muted
|
|
513
|
+
loop
|
|
514
|
+
></video>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
### Tracking impressions
|
|
518
|
+
|
|
519
|
+
Fire an impression event the first time an element is meaningfully visible. `threshold` sets what counts as "visible enough," and `once` guarantees a single event per element.
|
|
520
|
+
|
|
521
|
+
```svelte no-eval
|
|
522
|
+
<div
|
|
523
|
+
use:intersect={{ once: true, threshold: 0.5 }}
|
|
524
|
+
onintersect={() => analytics.track("ad_impression", { id: "banner-1" })}
|
|
525
|
+
>
|
|
526
|
+
<!-- ad content -->
|
|
527
|
+
</div>
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Infinite scroll
|
|
531
|
+
|
|
532
|
+
To detect when a user has scrolled to the end of a scrollable container, place a sentinel element after the content and set `root` to the container. `intersecting` becomes `true` once the sentinel scrolls into view.
|
|
533
|
+
|
|
534
|
+
**Note**: `root` must be the scrollable element itself (i.e. it has its own `overflow`/fixed `height`), not just an ancestor of one. If `root` merely sits inside a scrollable ancestor, the sentinel scrolls along with `root` and never changes position relative to it, so it reports as permanently intersecting.
|
|
535
|
+
|
|
536
|
+
```svelte
|
|
537
|
+
<script lang="ts">
|
|
538
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
539
|
+
|
|
540
|
+
let container: HTMLElement | undefined = $state();
|
|
541
|
+
let sentinel: HTMLElement | undefined = $state();
|
|
542
|
+
let reachedEnd = $state(false);
|
|
543
|
+
</script>
|
|
544
|
+
|
|
545
|
+
<header class:intersecting={reachedEnd}>
|
|
546
|
+
{reachedEnd ? "You've reached the end" : "Keep scrolling..."}
|
|
547
|
+
</header>
|
|
548
|
+
|
|
549
|
+
<div bind:this={container} style="height: 200px; overflow-y: auto;">
|
|
550
|
+
{#each Array.from({ length: 20 }) as _, i}
|
|
551
|
+
<p>Paragraph {i + 1}</p>
|
|
552
|
+
{/each}
|
|
553
|
+
|
|
554
|
+
<IntersectionObserver
|
|
555
|
+
element={sentinel}
|
|
556
|
+
root={container}
|
|
557
|
+
bind:intersecting={reachedEnd}
|
|
558
|
+
>
|
|
559
|
+
<div bind:this={sentinel} style="height: 1px;"></div>
|
|
560
|
+
</IntersectionObserver>
|
|
561
|
+
</div>
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The same sentinel pattern powers infinite scroll: call a `loadMore()` function from `onintersect` instead of (or alongside) updating `reachedEnd`.
|
|
565
|
+
|
|
566
|
+
### Reveal animation on scroll
|
|
567
|
+
|
|
568
|
+
Keep the `bind:this` element outside the `{#if intersecting}` block, and only gate the animated content inside it. The bound element stays in the DOM even before the reveal fires, so external triggers like `scrollIntoView()` still work, and the animation replays every time the element crosses into or out of view.
|
|
569
|
+
|
|
570
|
+
```svelte
|
|
571
|
+
<script lang="ts">
|
|
572
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
573
|
+
import { fly } from "svelte/transition";
|
|
574
|
+
|
|
575
|
+
let revealNode: HTMLElement | undefined = $state();
|
|
576
|
+
let revealIntersecting = $state(false);
|
|
577
|
+
</script>
|
|
578
|
+
|
|
579
|
+
<header class:intersecting={revealIntersecting}>
|
|
580
|
+
<p>{revealIntersecting ? "In view" : "Not in view"}</p>
|
|
581
|
+
<button
|
|
582
|
+
style="margin-top: 0.75rem;"
|
|
583
|
+
onclick={() => {
|
|
584
|
+
revealNode?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
585
|
+
}}
|
|
586
|
+
>
|
|
587
|
+
Scroll to section
|
|
588
|
+
</button>
|
|
589
|
+
</header>
|
|
590
|
+
|
|
591
|
+
<IntersectionObserver element={revealNode} bind:intersecting={revealIntersecting}>
|
|
592
|
+
<div bind:this={revealNode}>
|
|
593
|
+
{#if revealIntersecting}
|
|
594
|
+
<section transition:fly={{ y: 50, duration: 300 }}>
|
|
595
|
+
Hello world
|
|
596
|
+
</section>
|
|
597
|
+
{/if}
|
|
598
|
+
</div>
|
|
599
|
+
</IntersectionObserver>
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### `skip`
|
|
603
|
+
|
|
604
|
+
Set `skip` to `true` to unobserve without disconnecting the underlying observer or losing `entry`/`intersecting` state. Useful for pausing tracking on an off-screen carousel panel or a closed modal. Set `skip` back to `false` to resume; unlike `once`, this toggles back and forth freely. `MultipleIntersectionObserver` and the `intersect` action support the same `skip` option.
|
|
605
|
+
|
|
606
|
+
```svelte no-eval
|
|
607
|
+
<script lang="ts">
|
|
608
|
+
import IntersectionObserver from "svelte-intersection-observer";
|
|
609
|
+
|
|
610
|
+
let elementSkip: HTMLElement | undefined = $state();
|
|
611
|
+
let paused = $state(false);
|
|
612
|
+
</script>
|
|
613
|
+
|
|
614
|
+
<button onclick={() => (paused = !paused)}>
|
|
615
|
+
{paused ? "Resume" : "Pause"}
|
|
616
|
+
</button>
|
|
617
|
+
|
|
618
|
+
<IntersectionObserver element={elementSkip} skip={paused}>
|
|
619
|
+
{#snippet children({ intersecting })}
|
|
620
|
+
<div bind:this={elementSkip}>{intersecting ? "In view" : "Not in view"}</div>
|
|
621
|
+
{/snippet}
|
|
622
|
+
</IntersectionObserver>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### List strategy
|
|
626
|
+
|
|
627
|
+
Both [`MultipleIntersectionObserver`](#multipleintersectionobserver) and [`createIntersectionGroup`](#createintersectiongroup) share one observer across many elements. Pick based on how you want to wire it up:
|
|
628
|
+
|
|
629
|
+
- Use `MultipleIntersectionObserver` when you're fine wrapping the list in a component and want a ready-made `elementIntersections` map handed to you via the `children` snippet.
|
|
630
|
+
- Use `createIntersectionGroup` when you'd rather attach directly to each element with `{@attach}`, no extra component, and are happy tracking intersection state on your own per-item objects (as shown in its example above).
|
|
436
631
|
|
|
437
|
-
|
|
632
|
+
Either way, avoid giving `IntersectionObserver` a single shared `bind:this` variable inside `#each`; see the warning under [pooled component](#multipleintersectionobserver).
|
|
438
633
|
|
|
439
|
-
|
|
634
|
+
## `IntersectionObserverEntry`
|
|
440
635
|
|
|
441
636
|
Note that all properties in [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) are read-only.
|
|
442
637
|
|