Detect if an element is in the viewport using the Intersection Observer API.
Try it in the Svelte REPL.
# NPM
npm i svelte-intersection-observer
# pnpm
pnpm i svelte-intersection-observer
# Bun
bun i svelte-intersection-observer
# Yarn
yarn add svelte-intersection-observer
Use the bind:this directive to pass an element reference to the IntersectionObserver component.
Then, simply bind to the reactive intersecting prop to determine if the element intersects the viewport.
<script>
import IntersectionObserver from "svelte-intersection-observer";
let element;
let intersecting;
</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>Set once to true for the intersection event to occur only once. The element will be unobserved after the first intersection event occurs.
<script>
import IntersectionObserver from "svelte-intersection-observer";
let elementOnce;
let intersectOnce;
</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>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 can be toggled back and forth. MultipleIntersectionObserver and the intersect action support the same skip option.
<script>
import IntersectionObserver from "svelte-intersection-observer";
let element;
let paused = false;
</script>
<button on:click={() => (paused = !paused)}>
{paused ? "Resume" : "Pause"}
</button>
<IntersectionObserver {element} skip={paused} let:intersecting>
<div bind:this={element}>{intersecting ? "In view" : "Not in view"}</div>
</IntersectionObserver>An alternative to binding to the intersecting prop is to use the let: directive.
In the following example, the "Hello world" element will fade in when its containing element intersects the viewport.
<script>
import IntersectionObserver from "svelte-intersection-observer";
import { fade } from "svelte/transition";
let node;
</script>
<header />
<IntersectionObserver element={node} let:intersecting>
<div bind:this={node}>
{#if intersecting}
<div transition:fade={{ delay: 200 }}>Hello world</div>
{/if}
</div>
</IntersectionObserver>The observe event is dispatched when the element is first observed and also whenever an intersection event occurs.
<IntersectionObserver
{element}
on:observe={(e) => {
console.log(e.detail); // IntersectionObserverEntry
console.log(e.detail.isIntersecting); // true | false
}}
>
<div bind:this={element}>Hello world</div>
</IntersectionObserver>As an alternative to binding the intersecting prop, you can listen to the intersect event that is dispatched if the observed element is intersecting the viewport.
Note: Compared to on:observe, on:intersect is dispatched only when the element is intersecting the viewport. In other words, e.detail.isIntersecting will only be true.
<IntersectionObserver
{element}
on:intersect={(e) => {
console.log(e.detail); // IntersectionObserverEntry
console.log(e.detail.isIntersecting); // true
}}
>
<div bind:this={element}>Hello world</div>
</IntersectionObserver>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.
<script>
import IntersectionObserver from "svelte-intersection-observer";
let container;
let sentinel;
let reachedEnd;
</script>
<header class:intersecting={reachedEnd}>
{reachedEnd ? "You've reached the end" : "Keep scrolling..."}
</header>
<div bind:this={container} style:height="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;" />
</IntersectionObserver>
</div>As an alternative to the IntersectionObserver component, use the intersect action to observe an element directly with use:, without a bind:this reference or wrapper markup. Listen for on:observe/on:intersect on the observed element itself.
<script>
import { intersect } from "svelte-intersection-observer";
let actionIntersecting = false;
</script>
<header class:intersecting={actionIntersecting}>
{actionIntersecting ? "Element is in view" : "Element is not in view"}
</header>
<div
use:intersect={{ once: true }}
on:observe={(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.
For performance, use MultipleIntersectionObserver to observe multiple elements.
This avoids instantiating a new observer for every element.
<script>
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
let ref1;
let ref2;
$: elements = [ref1, ref2];
</script>
<MultipleIntersectionObserver {elements} let: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>
</MultipleIntersectionObserver>MultipleIntersectionObserver also handles a dynamic, #each-driven list — give every item its own slot in an array/object instead of one shared variable.
<script>
import { MultipleIntersectionObserver } from "svelte-intersection-observer";
let items = [
{ id: 1, text: "Item 1" },
{ id: 2, text: "Item 2" },
{ id: 3, text: "Item 3" },
];
let refs = [];
let itemsContainer;
$: itemElements = refs;
</script>
<MultipleIntersectionObserver
elements={itemElements}
root={itemsContainer}
let:elementIntersections
>
<header>
{#each items as item, i}
<div class:intersecting={elementIntersections.get(refs[i])}>
{item.text}: {elementIntersections.get(refs[i]) ? "✓" : "✗"}
</div>
{/each}
</header>
<div bind:this={itemsContainer}>
{#each items as item, i (item.id)}
<div bind:this={refs[i]}>{item.text}</div>
{/each}
</div>
</MultipleIntersectionObserver>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 and bound via bind:this={node} inside it). Every iteration overwrites the same node, so each observer instance keeps re-observing a moving target, which can produce an infinite update loop. Use MultipleIntersectionObserver with a per-item ref, as shown above, instead.
| 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 numbers between 0 and 1 |
0 |
| entry | Observed element metadata | null or IntersectionObserverEntry |
null |
| observer | IntersectionObserver instance |
null or 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, not something this component controls.
- on:observe: fired when the element is first observed or whenever an intersection change occurs
- on:intersect: fired when the element is intersecting the viewport
The e.detail dispatched by the observe and intersect events is an IntersectionObserverEntry interface.
| Name | Type |
|---|---|
| intersecting | boolean |
| entry | null or IntersectionObserverEntry |
| observer | IntersectionObserver |
| 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 numbers 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> |
new Map() |
| observer | IntersectionObserver instance |
null or IntersectionObserver |
null |
| skip | Pause observing all elements without losing state | boolean |
false |
- on:observe: fired when an element is first observed or whenever an intersection change occurs
- on:intersect: fired when an element is intersecting the viewport
The e.detail for both events includes:
{
entry: IntersectionObserverEntry;
target: HTMLElement;
}| Name | Type |
|---|---|
| observer | IntersectionObserver |
| elementIntersections | Map<HTMLElement | null, boolean> |
| elementEntries | Map<HTMLElement | null, IntersectionObserverEntry> |
| 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 numbers 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 |
- on:observe: fired on the element when it is first observed or whenever an intersection change occurs
- on:intersect: fired on the element when it is intersecting the viewport
The e.detail dispatched by the observe and intersect events is an IntersectionObserverEntry interface.
Note that all properties in IntersectionObserverEntry are read-only.
IntersectionObserverEntry
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;
};
}