svelte-intersection-observer
Version:
Detects when an element enters or exits the viewport using the Intersection Observer API.
685 lines (520 loc) • 31.9 kB
Markdown
# svelte-intersection-observer
<!-- HIDE_START -->
[![NPM][npm]][npm-url]
<!-- HIDE_END -->
## About
`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)).
It offers six interchangeable primitives, all backed by the same shared observer logic.
| Primitive | Export | Use it when... |
| :-------- | :----- | :--------------------- |
| [Component](#intersectionobserver) | `IntersectionObserver` | you want a component with a bound `intersecting` prop |
| [Pooled component](#multipleintersectionobserver) | `MultipleIntersectionObserver` | you're observing many elements and want one shared observer |
| [Action](#intersect) | `intersect` | you want `use:` on a plain element, no extra markup |
| [Attachment](#intersectattachment) | `intersectAttachment` | same as the action, composed via `{@attach}` (Svelte 5.29+) |
| [Composable](#createintersectionobserver) | `createIntersectionObserver` | you want reactive state from `<script>`, no directive |
| [Factory](#createintersectiongroup) | `createIntersectionGroup` | you're rendering a list with `{@attach}` and want one shared observer |
See [Library](#library) for the full docs on each. Try it in the [Svelte REPL](https://svelte.dev/repl/8cd2327a580c4f429c71f7df999bd51d).
### Compatibility
| Package version | Svelte version | Notes |
| :--------------- | :----------------- | :----------------------------------------- |
| [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 |
| 2.x | 5 (runes mode only) | Uses `$props()`, snippets, and callback props |
<!-- TOC -->
## Installation
```sh
# NPM
npm i svelte-intersection-observer
# pnpm
pnpm i svelte-intersection-observer
# Bun
bun i svelte-intersection-observer
# Yarn
yarn add svelte-intersection-observer
```
## Library
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.
### `IntersectionObserver`
Use the [`bind:this`](https://svelte.dev/docs#bind_element) directive to pass an element reference to the `IntersectionObserver` component.
Then bind to the reactive `intersecting` prop to check whether the element intersects the viewport.
```svelte
<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>
```
#### `once`
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.
```svelte
<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>
```
#### `children` snippet
An alternative to binding to the `intersecting` prop is to use the `children` snippet, which receives `intersecting`, `entry`, and `observer`.
In this example, "Hello world" fades in when its containing element intersects the viewport.
```svelte
<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>
```
#### Props
| Name | Description | Type | Default value |
| :----------- | :---------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ | :------------ |
| element | Observed element | `null` or `HTMLElement` | `null` |
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` | `false` |
| root | Containing element | `null` or `HTMLElement` | `null` |
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
| 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` |
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) | `null` |
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
| skip | Pause observing without losing `entry`/`intersecting` state | `boolean` | `false` |
**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.
#### Callback props
Same `onobserve`/`onintersect` behavior as described in [Callbacks](#callbacks-onobserve-and-onintersect) below.
#### `children` snippet props
| Name | Type |
| :----------- | :------------------------------------------------------------------------------------------------------------------ |
| intersecting | `boolean` |
| entry | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
| observer | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
### `MultipleIntersectionObserver`
For performance, use `MultipleIntersectionObserver` to observe multiple elements with one shared observer instead of instantiating one per element.
```svelte
<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>
```
#### Using with `#each`
`MultipleIntersectionObserver` also handles a dynamic, `#each`-driven list: give every item its own slot in an array/object instead of one shared variable.
```svelte
<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();
let itemElements = $derived(refs);
</script>
<MultipleIntersectionObserver elements={itemElements} 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>
```
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`.
**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.
#### Props
| Name | Description | Type | Default value |
| :------------------- | :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- | :------------ |
| elements | Array of HTML elements to observe | `Array<HTMLElement \| null>` | `[]` |
| once | Unobserve elements after the first intersection event | `boolean` | `false` |
| root | Containing element | `null` or `HTMLElement` | `null` |
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
| 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` |
| elementIntersections | Map of each element to its intersection state | `Map<HTMLElement \| null, boolean>` | `new Map()` |
| elementEntries | Map of each element to its latest entry | `Map<HTMLElement \| null,` [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)`>` | `new Map()` |
| observer | `IntersectionObserver` instance | `null` or [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | `null` |
| skip | Pause observing all elements without losing state | `boolean` | `false` |
#### Callback props
Called with:
```ts
{
entry: IntersectionObserverEntry;
target: HTMLElement;
}
```
See [Callbacks](#callbacks-onobserve-and-onintersect) for when each one fires.
#### `children` snippet props
| Name | Type |
| :------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| observer | [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) |
| elementIntersections | `Map<HTMLElement \| null, boolean>` |
| elementEntries | `Map<HTMLElement \| null,` [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)`>` |
### `intersect`
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.
```svelte
<script lang="ts">
import { intersect } from "svelte-intersection-observer";
let actionIntersecting = $state(false);
</script>
<header class:intersecting={actionIntersecting}>
{actionIntersecting ? "Element is in view" : "Element is not in view"}
</header>
<div
use:intersect={{ once: true }}
onobserve={(e) => {
actionIntersecting = e.detail.isIntersecting;
}}
>
Hello world
</div>
```
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.
#### Options
| Name | Description | Type | Default value |
| :--------- | :------------------------------------------------------- | :----------------------------------------------------------------- | :------------ |
| root | Containing element | `null` or `HTMLElement` | `null` |
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
| 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` |
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
| skip | Pause observing without disconnecting the observer | `boolean` | `false` |
#### Dispatched events
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).
### `intersectAttachment`
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:`.
Attachments have a few architectural advantages over actions:
- No separate `update()` lifecycle method; they rerun reactively like a `$effect`
- Just plain functions, so they're easier to compose and generate dynamically
- Can be forwarded through components as ordinary props, unlike actions
```svelte
<script lang="ts">
import { intersectAttachment } from "svelte-intersection-observer";
let attachmentIntersecting = $state(false);
</script>
<header class:intersecting={attachmentIntersecting}>
{attachmentIntersecting ? "Element is in view" : "Element is not in view"}
</header>
<div
{@attach intersectAttachment(() => ({ once: true }))}
onobserve={(e) => {
attachmentIntersecting = e.detail.isIntersecting;
}}
>
Hello world
</div>
```
**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.
Options and dispatched events are identical to the [`intersect` action](#intersect) above.
### `createIntersectionObserver`
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}`.
```svelte no-eval
<script lang="ts">
import { createIntersectionObserver } from "svelte-intersection-observer";
const observer = createIntersectionObserver(() => ({ threshold: 0.5 }));
</script>
<div {@attach observer.attach}>
{observer.intersecting ? "In view" : "Not in view"}
</div>
```
`createIntersectionObserver` takes the same options as [`intersectAttachment`](#intersectattachment) (as a function returning the options object) and reuses its underlying observer logic.
#### Return value
| Name | Description | Type |
| :----------- | :------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------ |
| intersecting | `true` if the observed element is intersecting the viewport | `boolean` |
| entry | Observed element metadata | `null` or [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) |
| attach | Attachment to apply to the observed element via `{@attach}` | [`Attachment<HTMLElement>`](https://svelte.dev/docs/svelte/svelte-attachments) |
### `createIntersectionGroup`
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.
```svelte
<script lang="ts">
import { createIntersectionGroup } from "svelte-intersection-observer";
let groupItems = $state(
Array.from({ length: 5 }, (_, i) => ({ id: i, intersecting: false })),
);
const group = createIntersectionGroup();
</script>
<header>
{#each groupItems as item (item.id)}
<div class:intersecting={item.intersecting}>
Item {item.id}: {item.intersecting ? "✓" : "✗"}
</div>
{/each}
</header>
{#each groupItems as item (item.id)}
<div
class:intersecting={item.intersecting}
{@attach group.attach({
onobserve: (entry) => (item.intersecting = entry.isIntersecting),
})}
>
Item {item.id}
</div>
{/each}
```
`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:
```js
const group = createIntersectionGroup(() => ({
root: container,
rootMargin: "0px",
threshold: 0.5,
}));
```
`once`, `skip`, `onobserve`, and `onintersect` are the only options that make sense per element, so those are what `group.attach(...)` accepts.
#### Signature
```ts
function createIntersectionGroup(
getSharedOptions?: () => IntersectGroupSharedOptions,
): IntersectionGroup;
```
#### Shared options
Passed once to `createIntersectionGroup`; apply to every element in the group.
| Name | Description | Type | Default value |
| :--------- | :----------------------------------------------------- | :----------------------------------------------------------------- | :------------ |
| root | Containing element | `null` or `HTMLElement` | `null` |
| rootMargin | Margin offset of the containing element | `string` | `"0px"` |
| 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` |
#### `group.attach(options)` per-node options
Passed once per element, to `group.attach(...)`.
| Name | Description | Type | Default value |
| :--------- | :---------------------------------------------------------- | :------------------------------------------------------- | :------------ |
| once | Unobserve the element after the first intersection event | `boolean` | `false` |
| skip | Skip observing this element without affecting the group | `boolean` | `false` |
| onobserve | Called when the element is first observed or when an intersection change occurs | `(entry: IntersectionObserverEntry) => void` | `undefined` |
| onintersect | Called when the element is intersecting the viewport | `(entry: IntersectionObserverEntry) => void` | `undefined` |
#### Callbacks: `onobserve` and `onintersect`
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`):
- **onobserve**: called when the element is first observed, and again on every intersection change
- **onintersect**: called only when the element is intersecting the viewport (a filtered view of `onobserve`)
```svelte no-eval
<IntersectionObserver
{element}
onobserve={(entry) => {
console.log(entry); // IntersectionObserverEntry
console.log(entry.isIntersecting); // true | false
}}
onintersect={(entry) => {
console.log(entry.isIntersecting); // always true
}}
>
<div bind:this={element}>Hello world</div>
</IntersectionObserver>
```
## Use Cases
Realistic scenarios built from the primitives above.
### Lazy-loading images
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.
```svelte no-eval
<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=""
/>
```
### Autoplaying video
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`.
```svelte no-eval
<script lang="ts">
import { intersect } from "svelte-intersection-observer";
let video: HTMLVideoElement | undefined = $state();
</script>
<video
bind:this={video}
use:intersect
onobserve={(e) => {
e.detail.isIntersecting ? video?.play() : video?.pause();
}}
src="/clip.mp4"
muted
loop
></video>
```
### Tracking impressions
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.
```svelte no-eval
<div
use:intersect={{ once: true, threshold: 0.5 }}
onintersect={() => analytics.track("ad_impression", { id: "banner-1" })}
>
<!-- ad content -->
</div>
```
### Infinite scroll
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.
**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.
```svelte
<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>
```
The same sentinel pattern powers infinite scroll: call a `loadMore()` function from `onintersect` instead of (or alongside) updating `reachedEnd`.
### Reveal animation on scroll
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.
```svelte
<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>
```
### `skip`
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.
```svelte no-eval
<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>
```
### List strategy
Both [`MultipleIntersectionObserver`](#multipleintersectionobserver) and [`createIntersectionGroup`](#createintersectiongroup) share one observer across many elements. Pick based on how you want to wire it up:
- 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.
- 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).
Either way, avoid giving `IntersectionObserver` a single shared `bind:this` variable inside `#each`; see the warning under [pooled component](#multipleintersectionobserver).
## `IntersectionObserverEntry`
Note that all properties in [IntersectionObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry) are read-only.
<details>
<summary><code>IntersectionObserverEntry</code></summary>
```ts
interface IntersectionObserverEntry {
target: HTMLElement;
time: number;
isIntersecting: boolean;
isVisible: boolean;
intersectionRatio: number;
intersectionRect: {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
x: number;
y: number;
};
rootBounds: {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
x: number;
y: number;
};
boundingClientRect: {
bottom: number;
height: number;
left: number;
right: number;
top: number;
width: number;
x: number;
y: number;
};
}
```
</details>
[npm]: https://img.shields.io/npm/v/svelte-intersection-observer.svg?color=%23ff3e00&style=for-the-badge
[npm-url]: https://npmjs.com/package/svelte-intersection-observer