Primitives to support using the intersection observer API.
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 3 | Display & Media | 3.0.0-next.3 (next) | Aug 12, 2026 | Demo → |
npm i @solid-primitives/intersection-observer@nextA range of IntersectionObserver API utilities great for different types of use cases:
makeIntersectionObserver- A non-reactive, imperative wrapper around the IntersectionObserver API.createIntersectionObserver- A reactive observer primitive.createViewportObserver- More advanced tracker that creates a store of element signals.createVisibilityObserver- Basic visibility observer using a signal.
makeIntersectionObserver
A non-reactive, imperative wrapper around the native IntersectionObserver API. Useful when you need full manual control over observation lifecycle without integrating into a Solid reactive scope.
import { makeIntersectionObserver } from "@solid-primitives/intersection-observer";
const { add, remove, start, stop, reset, instance } = makeIntersectionObserver( [el1, el2], entries => { entries.forEach(e => console.log(e.isIntersecting)); }, { threshold: 0.5 },);
add(el3);remove(el1);stop(); // disconnects the observerDefinition
function makeIntersectionObserver( elements: Element[], onChange: IntersectionObserverCallback, options?: IntersectionObserverInit,): { add: (el: Element) => void; remove: (el: Element) => void; start: () => void; reset: () => void; stop: () => void; instance: IntersectionObserver;};createIntersectionObserver
Returns a tuple of:
- A store array of
IntersectionObserverEntryobjects — one slot per element, updated in place when that element's intersection state changes. Readingentries[i].isIntersectingonly re-runs the computation that reads sloti. isVisible(el)— a pending-aware helper that throwsNotReadyErroruntil the first observation fires for that element, then returnsentry.isIntersectingreactively. Integrates with<Loading>for a natural loading fallback.
import { createIntersectionObserver } from "@solid-primitives/intersection-observer";
const [targets, setTargets] = createSignal<Element[]>([]);
const [entries, isVisible] = createIntersectionObserver(targets, { threshold: 0.5 });
// entries — reactive store, fine-grained per-element tracking:createEffect(() => { entries.forEach(e => console.log(e.isIntersecting));});
// isVisible — integrates with <Loading> for pending state:<Loading fallback={<p>Checking…</p>}> <Show when={isVisible(el)}><p>Visible!</p></Show></Loading>
<div ref={el => setTargets(p => [...p, el])} />;options may be a reactive accessor — if the options object changes, the observer is disconnected and recreated with the new options, and all currently tracked elements are re-observed.
Definition
function createIntersectionObserver( elements: Accessor<Element[]>, options?: MaybeAccessor<IntersectionObserverInit>,): readonly [entries: readonly IntersectionObserverEntry[], isVisible: (el: Element) => boolean];createViewportObserver
This primitive comes with a number of flexible options. You can specify a callback at the root with an array of elements or individual callbacks for individual elements.
The add function has two forms:
add(el, callback)— imperative: register an element with its callback directly.add(callback)— curried ref form: returns a(el) => voidref callback for use asref={add(e => ...)}in JSX.
import { createViewportObserver } from '@solid-primitives/intersection-observer';
// Basic usage:const [add, { remove, start, stop, instance }] = createViewportObserver(els, e => {...});add(el, e => console.log(e.isIntersecting));
// Ref usage (replaces old use: directive):const [add] = createViewportObserver();<div ref={add(e => console.log(e.isIntersecting))}></div>Definition
function createViewportObserver( elements: MaybeAccessor<Element[]>, callback: EntryCallback, options?: IntersectionObserverInit,): CreateViewportObserverReturnValue;function createViewportObserver( initial: MaybeAccessor<[Element, EntryCallback][]>, options?: IntersectionObserverInit,): CreateViewportObserverReturnValue;function createViewportObserver( options?: IntersectionObserverInit,): CreateViewportObserverReturnValue;createVisibilityObserver
Creates a reactive signal that changes when a single element's visibility changes. Takes the element to observe directly — the previous curried factory pattern has been removed.
The element may be a reactive accessor (() => el) or a plain DOM element. Passing a falsy accessor value removes the element from the observer.
When initialValue is omitted, visible() throws NotReadyError until the first IntersectionObserver callback fires — integrating naturally with <Loading> for a loading fallback:
import { createVisibilityObserver } from "@solid-primitives/intersection-observer";
let el: HTMLDivElement | undefined;
const visible = createVisibilityObserver(() => el, { threshold: 0.8 });
// Pending until first IO fires — shows fallback in the meantime:<Loading fallback={<p>Checking…</p>}> <Show when={visible()} fallback={<p>Hidden</p>}> <p>Visible!</p> </Show></Loading>;Provide initialValue to opt out of the pending state and start with a known value:
const visible = createVisibilityObserver(() => el, { initialValue: false });// visible() === false immediately, no pending state<div>{visible() ? "Visible" : "Hidden"}</div>;Options accepted in addition to IntersectionObserverInit:
initialValue— Opt-in initial value; when omitted,visible()throwsNotReadyErroruntil the first observation.
Setter callback
createVisibilityObserver accepts an optional setter callback as the third argument. It is called when the element's intersection changes and should return a boolean indicating whether the element is visible.
let el: HTMLDivElement | undefined;
const visible = createVisibilityObserver( () => el, { threshold: 0.8 }, entry => { // do some calculations on the intersection entry return entry.isIntersecting; },);Exported modifiers
withOccurrence
Provides information about element occurrence in the viewport — "Entering", "Leaving", "Inside" or "Outside".
import { createVisibilityObserver, withOccurrence } from "@solid-primitives/intersection-observer";
let el: HTMLDivElement | undefined;
const visible = createVisibilityObserver( () => el, { threshold: 0.8 }, withOccurrence((entry, { occurrence }) => { console.log(occurrence); // => "Entering" | "Leaving" | "Inside" | "Outside" return entry.isIntersecting; }),);withDirection
Provides information about element direction on the screen — "Left", "Right", "Top", "Bottom" or "None".
import { createVisibilityObserver, withDirection } from "@solid-primitives/intersection-observer";
let el: HTMLDivElement | undefined;
const visible = createVisibilityObserver( () => el, { threshold: 0.8 }, withDirection((entry, { directionY, directionX, visible }) => { if (!entry.isIntersecting && directionY === "Top" && visible) { return true; } return entry.isIntersecting; }),);Definition
function createVisibilityObserver( element: Accessor<Element | FalsyValue> | Element, options?: IntersectionObserverInit & { initialValue?: boolean }, setter?: MaybeAccessor<VisibilitySetter>,): Accessor<boolean>;Changelog
See CHANGELOG.md