@svelte-put/intersect

GitHub Github

svelte action that wraps for IntersectionObserver

@svelte-put/intersect @svelte-put/intersect @svelte-put/intersect @svelte-put/intersect @svelte-put/intersect

Still on Svelte 4? See the old docs site here.

Acknowledgement

This packages employs the Svelte action strategy. If you are looking for a declarative, component-oriented solution, check out metonym's implementation.

Installation

npm install --save-dev @svelte-put/intersect
pnpm add -D @svelte-put/intersect
yarn add -D @svelte-put/intersect

New to Svelte 5? See Migration Guides.

Quick Start

Below is a minimal example with default options. An intersect CustomEvent is fired when the targeted div intersects viewport.

<script lang="ts">
	import { intersect, type IntersectDetail } from '@svelte-put/intersect';

	function onIntersect(e: CustomEvent<IntersectDetail>) {
		const { observer, entries, direction } = e.detail;
		console.log('the observer itself', observer);
		console.log('scrolling direction:', direction);
		console.log('intersecting:', entries[0]?.isIntersecting ? 'entering' : 'leaving');
	}
</script>

<div use:intersect onintersect={onIntersect}>...</div>

Initialization Options

intersect can take a config object that supports all options in IntersectionObserver's constructor (root, rootMargin, threshold), and an additional enabled option to dynamically enable/disable the action.

<script>
	import { intersect } from '@svelte-put/intersect';
	let root;
</script>

<main bind:this={root}>
	<section
		use:intersect={{
			enabled: true,
			root,
			rootMargin: '100px 0px 50px 0px',
			threshold: [0.2, 0.5, 1],
		}}
	></section>
</main>

Intersection CustomEvents

intersect

Listener for the intersect CustomEvent is essentially the same callback as one passed to the IntersectionObserver's constructor.

intersect will attempt to detect the scrolling direction by keeping record of boundingClientRect. This is available in the event listener via the event.detail.direction property.

Example

Scroll down / up so that this box goes in and out of the viewport.

Observe the text indicator changes accordingly near the edge of screen.

<script lang="ts">
	import { intersect, type IntersectDetail } from '@svelte-put/intersect';

	let detail: IntersectDetail | undefined = $state();
	function onIntersect(e: CustomEvent<IntersectDetail>) {
		detail = e.detail;
	}
</script>

<div
	class="hl-info mx-auto flex h-80 w-4/5 flex-col items-center justify-between"
	use:intersect={{ threshold: 0.5, rootMargin: '-100px 0px 0px' }}
	onintersect={onIntersect}
>
	{#each new Array(2) as _}
		<p>
			{#if detail}
				<span>Scrolling {detail?.direction}</span> &
				<span>{detail?.entries[0]?.isIntersecting ? 'entering' : 'leaving'}</span>
			{/if}
		</p>
	{/each}
</div>

intersectonce

Following the same interface as with intersect, intersectonce CustomEvent only fires once on the first intersection and never again. One possible use case is for fading-in animation on scroll, as seen in the below example.

Example

Scroll down within this box and observe the fade-in intro effect!

<script lang="ts">
	import { intersect } from '@svelte-put/intersect';
	import { fly } from 'svelte/transition';

	let ulElement: HTMLUListElement | undefined = $state();
	let intersectionMap: Record<string, boolean> = $state({
		one: false,
		two: false,
		three: false,
		four: false,
		five: false,
		six: false,
		seven: false,
		eight: false,
	});
</script>

<ul
	class="max-h-[400px] w-full space-y-20 overflow-hidden overflow-y-auto p-4"
	bind:this={ulElement}
>
	{#each Object.keys(intersectionMap) as key (key)}
		{#key intersectionMap[key]}
			<li
				class="odd:bg-success-fg even:bg-info-fg h-[300px] marker:content-none"
				class:invisible={!intersectionMap[key]}
				in:fly={{ y: 100, duration: 250 }}
				use:intersect={{ threshold: 0.4, root: ulElement }}
				onintersectonce={() => (intersectionMap[key] = true)}
			></li>
		{/key}
	{/each}
</ul>

Migration Guides

V3 -> V4 (Svelte 5 in Runes mode)

When migrating to v4, you just need to update event attribute syntax to remove colon ::

<div use:intersect on:intersect on:intersectonce></div>
<div use:intersect onintersect onintersectonce></div>

Happy intersecting! 👨‍💻

Edit this page on Github