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

@terrahq/lazy

v0.0.1

Published

**[Live Demo](https://terra-lazy.netlify.app/)**

Readme

@terrahq/lazy

Live Demo

Lightweight lazy loading library built on IntersectionObserver. Zero dependencies.

Supports <img>, <picture>, <video>, <iframe>, and background images.

Install

npm install @terrahq/lazy

Quick Start

<img class="g--lazy-01" data-src="image.jpg" alt="..." />
import Lazy from '@terrahq/lazy';

const lazy = new Lazy();

That's it. Images load automatically when they enter the viewport.


HTML

Add the class g--lazy-01 and use data-src instead of src:

Image

<img class="g--lazy-01" data-src="photo.jpg" alt="..." />

Image with srcset

<img class="g--lazy-01" data-src="photo.jpg" data-srcset="photo-2x.jpg 2x" alt="..." />

Picture

<picture class="g--lazy-01">
    <source data-srcset="photo.avif" type="image/avif" />
    <source data-srcset="photo.webp" type="image/webp" />
    <img data-src="photo.jpg" alt="..." />
</picture>

Background image

<div class="g--lazy-01" data-src="hero.jpg"></div>

Video

<!-- Direct src -->
<video class="g--lazy-01" data-src="clip.mp4" muted autoplay loop playsinline></video>

<!-- Multiple sources -->
<video class="g--lazy-01" muted autoplay loop playsinline>
    <source data-src="clip.webm" type="video/webm" />
    <source data-src="clip.mp4" type="video/mp4" />
</video>

Iframe

<iframe class="g--lazy-01" data-src="https://www.youtube.com/embed/VIDEO_ID"></iframe>

CSS

Minimal recommended styles:

.g--lazy-01 {
    opacity: 0;
    transition: opacity 0.4s ease;
}

.g--lazy-01--is-loading {
    opacity: 0.3;
}

.g--lazy-01--is-active {
    opacity: 1;
}

.g--lazy-01--is-error {
    opacity: 1;
    outline: 2px solid red;
}

Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | selector | string | '.g--lazy-01' | CSS selector for elements to observe | | src | string | 'data-src' | Attribute name for src | | srcset | string | 'data-srcset' | Attribute name for srcset | | loadingClass | string | 'g--lazy-01--is-loading' | Class added while loading | | successClass | string | 'g--lazy-01--is-active' | Class added on successful load | | errorClass | string | 'g--lazy-01--is-error' | Class added on error | | root | Element\|null | null | IntersectionObserver root (null = viewport) | | rootMargin | string | '100px 0px' | Margin around root (loads before entering viewport) | | threshold | number | 0 | Intersection ratio to trigger (0 = first pixel) | | loadInvisible | boolean | false | Whether to observe hidden elements (display:none) |

Callbacks

| Callback | Arguments | When it fires | |----------|-----------|---------------| | onLoading | (element) | Element starts loading | | onSuccess | (element) | Element loaded successfully | | onError | (element) | Element failed to load | | onComplete | none | All observed elements have been processed | | onDestroy | none | destroy() is called (manual or automatic) | | onRevalidate | (newCount) | revalidate() finishes, receives count of new elements found |

const lazy = new Lazy({
    selector: '.g--lazy-01',
    rootMargin: '200px 0px',
    loadInvisible: true,
    onLoading: (el) => console.log('Loading:', el),
    onSuccess: (el) => console.log('Loaded:', el),
    onError: (el) => console.warn('Failed:', el),
    onComplete: () => console.log('All elements processed'),
    onDestroy: () => console.log('Observer disconnected'),
    onRevalidate: (count) => console.log(`Found ${count} new elements`),
});

Methods

revalidate()

Re-scans the DOM for new elements matching the selector. Skips elements that already have successClass or errorClass. Useful after dynamically adding content or when elements become visible (e.g., slider transitions).

container.innerHTML += '<img class="g--lazy-01" data-src="new.jpg" />';
lazy.revalidate();

load(element, force?)

Manually trigger loading of a specific element. Bypasses the IntersectionObserver — loads immediately.

lazy.load(document.querySelector('#my-image'));

// Force reload even if already loaded
lazy.load(document.querySelector('#my-image'), true);

destroy()

Disconnects the observer and cleans up. Called automatically when all observed elements have been processed. Call it manually when you need to tear down (e.g., page transitions).

lazy.destroy();

Examples

Basic — auto-load on scroll

import Lazy from '@terrahq/lazy';

const lazy = new Lazy({
    selector: '.g--lazy-01',
    rootMargin: '200px 0px',
    onSuccess: (el) => console.log('Loaded:', el.src),
});

Manual trigger — click to load

<button id="btn">Load image</button>
<img id="hero" class="g--lazy-manual" data-src="hero.jpg" alt="..." />
import Lazy from '@terrahq/lazy';

const lazy = new Lazy({
    selector: '.g--lazy-manual',
    successClass: 'g--lazy-01--is-active',
    loadingClass: 'g--lazy-01--is-loading',
    errorClass: 'g--lazy-01--is-error',
});

// Disconnect auto-observation — only load via .load()
lazy.destroy();

document.querySelector('#btn').addEventListener('click', () => {
    lazy.load(document.querySelector('#hero'));
});

With a slider (tiny-slider)

After each slide transition, call revalidate() so newly visible slides get observed:

import Lazy from '@terrahq/lazy';
import { tns } from 'tiny-slider';

const lazy = new Lazy();

const slider = tns({
    container: '#slider',
    items: 3,
    loop: false,
});

slider.events.on('transitionEnd', () => {
    lazy.revalidate();
});

With a marquee (@andresclua/infinite-marquee-gsap)

Marquee items move via GSAP transforms — IntersectionObserver detects them naturally as they enter the viewport. No revalidate() needed:

import Lazy from '@terrahq/lazy';
import gsap from 'gsap';
import { horizontalLoop } from '@andresclua/infinite-marquee-gsap';

const lazy = new Lazy();

const marqueeEl = document.querySelector('#marquee');
const loop = horizontalLoop(marqueeEl.children, {
    paused: false,
    repeat: -1,
    speed: 1,
});

// Pause on hover
marqueeEl.addEventListener('mouseenter', () => {
    gsap.to(loop, { timeScale: 0, overwrite: true });
});
marqueeEl.addEventListener('mouseleave', () => {
    gsap.to(loop, { timeScale: 1, overwrite: true });
});

SPA / page transitions (Swup, Barba, etc.)

Destroy on page leave, create a new instance on page enter:

import Lazy from '@terrahq/lazy';

let lazy;

// On page enter
function onContentReplaced() {
    lazy = new Lazy({
        selector: '.g--lazy-01',
        loadInvisible: true,
        onComplete: () => console.log('Page images ready'),
    });
}

// On page leave
function onWillReplaceContent() {
    if (lazy) {
        lazy.destroy();
        lazy = null;
    }
}

How It Works

Lifecycle

constructor()
  └─ _init() → querySelectorAll(selector) → IntersectionObserver.observe()

Element enters viewport:
  └─ onLoading(el) → adds loadingClass
  └─ _loadElement(el) → detects type (img/picture/video/iframe/background)
  └─ preloads via new Image() (images & backgrounds)
      ├─ success → onSuccess(el) → removes loadingClass, adds successClass
      └─ error   → onError(el)   → removes loadingClass, adds errorClass

All elements processed:
  └─ onComplete()
  └─ destroy() → onDestroy() → observer.disconnect()

Element type detection

| Element | How it loads | |---------|-------------| | <img> | Preloads with new Image(), then sets src/srcset on the real element | | <picture> | Sets srcset on each <source>, then loads the inner <img> | | <video> | Sets src on <source> children and/or the element itself, calls .load() | | <iframe> | Sets src directly, listens for onload/onerror | | Any other | Preloads with new Image(), then applies as background-image |

Auto-cleanup

After each element loads (success or error), the internal counter decrements. When it reaches zero, onComplete fires and destroy() is called automatically — the observer disconnects and no longer consumes resources.

Scroll direction

IntersectionObserver is direction-agnostic. It detects geometric intersection between the element and the root, regardless of whether the element became visible via vertical scroll, horizontal scroll, CSS transforms, or layout changes. Works with sliders, marquees, and any scroll direction.


Browser Support

All modern browsers. Uses IntersectionObserver (Chrome 51+, Firefox 55+, Safari 12.1+, Edge 15+).

Development

npm run dev      # Start dev server with demo page
npm run build    # Build to dist/ (ES + UMD)

License

MIT