npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wcstack/intersection

v1.13.0

Published

Declarative IntersectionObserver component for Web Components. Framework-agnostic visibility primitive via wc-bindable-protocol.

Readme

@wcstack/intersection

@wcstack/intersection is a headless IntersectionObserver component for the wcstack ecosystem.

It is not a visual UI widget. It is an async primitive node that turns an element's visibility into reactive state — the same way @wcstack/fetch turns a network request into reactive state and @wcstack/geolocation turns the device's location into reactive state.

With @wcstack/state, <wcs-intersect> can be bound directly through path contracts:

  • input / command surface: target, root, root-margin, threshold, once, manual, trigger
  • output state surface: entry, intersecting, ratio, visible, observing

This means visibility-aware work — lazy-loading, infinite scroll, scroll-spying — can be expressed declaratively in HTML, without writing new IntersectionObserver(), observe(), disconnect(), or teardown glue in your UI layer.

@wcstack/intersection follows the CSBC (Core / Shell / Binding Contract) architecture:

  • Core (IntersectionCore) owns the observer, entry normalization, the visible latch, and observation lifecycle
  • Shell (<wcs-intersect>) resolves what to observe from the DOM, manages display, lifecycle, and declarative commands
  • Binding Contract (static wcBindable) declares observable properties, writable inputs, and callable commands

Why this exists

IntersectionObserver is different from every other @wcstack sensor: the thing it observes is a DOM element, not a headless resource. Imperatively, wiring it up means creating an observer, resolving a target node, handling the entry callback, and tearing it all down on disconnect.

@wcstack/intersection moves that logic into a reusable component and exposes the result as bindable state. An element scrolling into view becomes a state transition, not imperative callback wiring. It is a read-only producer: the element/layout only produces values for the state (element/layout → state), with no path back.

The target attribute decides everything

target is the single knob that selects what is observed — and, with it, how <wcs-intersect> renders. It never injects a layout box unless you explicitly ask for one:

| target | observes | display | use case | |-------------------|-----------------------|-------------|----------------------| | omitted | first element child | contents | lazy-load wrapper | | "#hero" / sel. | the matched element | none | scrollspy (single) | | "self" | the element itself | block | infinite-scroll edge |

display:contents means wrapping a child injects no box of its own — so <wcs-intersect><img></wcs-intersect> does not disturb a flex/grid parent. Only the explicit target="self" sentinel takes a box.

First element child. When target is omitted, the first element child is observed. The target is re-resolved on every observe() (which runs on connect and on each observed-attribute change), so adding or removing the first child after connect switches the observed element on the next re-observe. If there is no element child at resolution time, it falls back to observing itself (display:block). Observing multiple targets at once is intentionally out of scope — wrap each target in its own <wcs-intersect>.

Install

npm install @wcstack/intersection

Quick Start

1. Lazy-load an image (visible latch)

visible flips to true the first time the target intersects and stays true. Bind the image src to it and the image only loads once it scrolls into view.

<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
<script type="module" src="https://esm.run/@wcstack/intersection/auto"></script>

<wcs-state>
  <script type="module">
    export default {
      shown: false,
      get src() {
        return this.shown ? "/photo.jpg" : "";
      }
    };
  </script>
</wcs-state>

<wcs-intersect once data-wcs="visible: shown">
  <img data-wcs="src: src" alt="lazy">
</wcs-intersect>

once disconnects the observer after the first intersection — ideal for one-shot lazy loads.

2. Infinite scroll (sentinel)

Place an empty target="self" marker at the bottom of a list; bind intersecting to a state flag that triggers loading more.

<ul data-wcs="for: items">
  <li data-wcs="textContent: items.*.name"></li>
</ul>

<wcs-intersect target="self" data-wcs="intersecting: atEnd"></wcs-intersect>
export default {
  items: [],
  atEnd: false,
  get _loadMore() {
    // a computed/effect that reacts to atEnd becoming true
    return this.atEnd ? fetchNextPage() : null;
  }
};

3. Scrollspy (single section)

Point target at a section elsewhere in the document; bind intersecting to highlight the matching nav item.

<nav>
  <a href="#features" data-wcs="class.active: featuresVisible">Features</a>
</nav>

<section id="features">…</section>

<wcs-intersect target="#features" threshold="0.5"
  data-wcs="intersecting: featuresVisible"></wcs-intersect>

Attributes

| Attribute | Type | Default | Description | |----------------|---------|------------|-------------| | target | string | (omitted)| What to observe: omitted → first child, a selector → that element, self → this element. | | root | string | (viewport) | Selector for the scroll root. | | root-margin | string | 0px | Margin around the root (CSS-margin syntax). | | threshold | string | 0 | A single ratio (0.5) or comma list (0,0.5,1) of 0..1 thresholds. Invalid / out-of-range values are dropped. | | once | boolean | false | Disconnect after the first intersecting observation. | | manual | boolean | false | Do not auto-observe on connect; drive it via commands instead. |

trigger has no attribute — it is a momentary command-property meant for @wcstack/state wiring only. A false → true write re-runs observe() and the property auto-resets to false (a one-shot acknowledgement; read observing for the actual outcome). Prefer the command-token protocol (command.observe: …) over this boolean for state-driven observation.

Output state

| Property | Type | Description | |----------------|----------------------------|-------------| | entry | WcsIntersectEntry \| null| Plain snapshot of the latest IntersectionObserverEntry (rects normalized to plain numbers), plus the live target node. | | intersecting | boolean | Whether the target currently intersects the root. | | ratio | number | The latest intersectionRatio. | | visible | boolean | Latch: true once the target has intersected; cleared only by reset(). | | observing | boolean | Whether an observation is currently active. |

Commands

| Command | Description | |---------------|-------------| | observe() | Re-resolve target / root from the DOM and (re)start observing. Idempotent: an unchanged target+options is a no-op (no fresh callback). | | reobserve() | Force a fresh observation even when target / options are unchanged — tears the observer down and rebuilds it, so a new initial callback fires for the current visibility. Use to re-arm an edge-driven consumer (e.g. infinite scroll) after the layout shifted without a visibility transition. observing stays true across a successful re-arm (no false blip). | | unobserve() | Stop observing the current target. | | disconnect()| Stop all observation. | | reset() | Clear the visible latch so a later intersection can set it again. |

Binding Contract (wcBindable)

Both the Core and the Shell declare the wc-bindable protocol.

// IntersectionCore (headless)
IntersectionCore.wcBindable = {
  protocol: "wc-bindable",
  version: 1,
  properties: [
    { name: "entry", event: "wcs-intersect:change" },
    { name: "intersecting", event: "wcs-intersect:change", getter: (e) => e.detail.isIntersecting },
    { name: "ratio", event: "wcs-intersect:change", getter: (e) => e.detail.intersectionRatio },
    { name: "visible", event: "wcs-intersect:visible-changed" },
    { name: "observing", event: "wcs-intersect:observing-changed" },
  ],
  commands: [
    { name: "observe" }, { name: "reobserve" }, { name: "unobserve" }, { name: "disconnect" }, { name: "reset" },
  ],
};

The Shell (<wcs-intersect>) inherits the Core's properties / commands, adds the momentary trigger property, and declares the DOM-driven inputs (target, root, rootMargin, threshold, once, manual, trigger).

Using the Core standalone

IntersectionCore is framework-agnostic and can be used without the custom element. You hand it the element to observe (the Shell does this resolution for you):

import { IntersectionCore } from "@wcstack/intersection";

const core = new IntersectionCore();
core.addEventListener("wcs-intersect:change", (e) => {
  console.log(e.detail.isIntersecting, e.detail.intersectionRatio);
});
core.observe(document.querySelector("#hero"), { threshold: [0, 0.5, 1] });
// later
core.disconnect();

Notes & limitations

  • Single target. Each <wcs-intersect> observes exactly one element so the state maps to a single value surface. For many targets, use many elements.
  • Never throws. Unsupported environments (no IntersectionObserver) and invalid options (e.g. a malformed root-margin) are silent no-ops: observing stays false rather than throwing.
  • No permission gate / secure context requirement (unlike @wcstack/geolocation).

License

MIT