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

lazy-progressive-image

v1.0.10

Published

Universal progressive image loading component for Web Components and React.

Readme

lazy-progressive-image

npm

A universal progressive image component for Web Components, React, Vue, and Svelte.

It renders a blurred thumbnail first, lazy-loads the full image when visible, and swaps to a fallback icon when the full image is missing or errors.

Features

  • Progressive loading with blur thumb -> full image transition
  • IntersectionObserver visibility trigger with preload margin
  • Fallback rendering for empty or broken full-size images
  • Thumb failure isolation (thumb can fail without hiding full image)
  • Universal: vanilla HTML/JS web component + React wrapper
  • TypeScript types included

How Lazy Loading Works

  1. The component mounts a wrapper element and watches it with IntersectionObserver.
  2. The thumbnail (if provided) renders immediately as a placeholder, independent of visibility — it stays mounted until the full image has actually loaded.
  3. The full image is not rendered (and not fetched) until the wrapper is near/in view (rootMargin: 100px) — unless that exact src has already finished loading elsewhere on the page during this session, in which case it renders right away without waiting for intersection.
  4. On full image load, the component calls HTMLImageElement.decode() when available.
  5. Once decode completes (or immediately after load on browsers without decode), the full image fades in and the thumbnail fades out via CSS transition.

This means visibility (or a known-loaded cache hit) controls network/render timing, and decode() helps avoid showing a partially decoded full image.

Install

pnpm add lazy-progressive-image

or

npm i lazy-progressive-image

Usage

Vanilla HTML/JS

Import the web component and use it directly in HTML:

<!doctype html>
<html lang="en">
  <head>
    <script type="importmap">
      {
        "imports": {
          "lit": "https://esm.sh/[email protected]",
          "lit/": "https://esm.sh/[email protected]/"
        }
      }
    </script>
    <script type="module">
      import "lazy-progressive-image";
    </script>
  </head>
  <body>
    <lazy-progressive-image
      src="https://example.com/full.jpg"
      thumbnail="https://example.com/thumb.jpg"
      alt="Example image"
      root-margin="100px"
    ></lazy-progressive-image>
  </body>
</html>

React

The /react export wraps the web component with typed props and React-style event handlers.

import { LazyProgressiveImage } from "lazy-progressive-image/react";

export function Example() {
  return (
    <LazyProgressiveImage
      src="https://example.com/full.jpg"
      thumbnail="https://example.com/thumb.jpg"
      alt="Example image"
    />
  );
}

In React 19 and later, you can also use the custom element directly without the wrapper:

import "lazy-progressive-image";

export function Example() {
  return (
    <lazy-progressive-image
      src="https://example.com/full.jpg"
      thumbnail="https://example.com/thumb.jpg"
      alt="Example image"
      root-margin="100px"
    ></lazy-progressive-image>
  );
}

Use the wrapper when you want TypeScript JSX types and onLoad/onError props. Use the direct element when you prefer zero abstraction and only need standard attributes.

If you want to reference the React props type explicitly, import it from the React entrypoint:

import {
  LazyProgressiveImage,
  type LazyProgressiveImageProps,
} from "lazy-progressive-image/react";

const props: LazyProgressiveImageProps = {
  src: "https://example.com/full.jpg",
  thumbnail: "https://example.com/thumb.jpg",
  alt: "Example image",
};

Vue

<script setup>
import { LazyProgressiveImage } from "lazy-progressive-image";
</script>

<template>
  <lazy-progressive-image
    src="https://example.com/full.jpg"
    thumbnail="https://example.com/thumb.jpg"
    alt="Example image"
    root-margin="100px"
  ></lazy-progressive-image>
</template>

Svelte

<script>
  import "lazy-progressive-image";
</script>

<lazy-progressive-image
  src="https://example.com/full.jpg"
  thumbnail="https://example.com/thumb.jpg"
  alt="Example image"
  root-margin="100px"
></lazy-progressive-image>

Listening for image load events

The component dispatches a bubbling, composed image-loaded custom event whenever an image finishes loading. The event detail tells you which image variant loaded and its URL:

| Field | Type | Description | | ------------- | ----------------------- | -------------------------------------------------------- | | detail.src | string \| undefined | The URL of the image that loaded (src or thumbnail). | | detail.type | "full" \| "thumbnail" | Which variant loaded. |

Vanilla HTML/JS

const image = document.querySelector("lazy-progressive-image");

image.addEventListener("image-loaded", (event) => {
  const { src, type } = event.detail;
  console.log(`${type} image loaded:`, src);
});

React

With the React wrapper, use the onLoad prop:

import { LazyProgressiveImage } from "lazy-progressive-image/react";

<LazyProgressiveImage
  src="https://example.com/full.jpg"
  thumbnail="https://example.com/thumb.jpg"
  alt="Example image"
  onLoad={(event) => {
    const { src, type } = (event as CustomEvent).detail;
    console.log(`${type} image loaded:`, src);
  }}
/>;

When using the custom element directly in React 19, attach the listener via ref:

import "lazy-progressive-image";

<lazy-progressive-image
  ref={(el) => {
    el?.addEventListener("image-loaded", (e) => {
      const { src, type } = (e as CustomEvent).detail;
      console.log(`${type} image loaded:`, src);
    });
  }}
  src="https://example.com/full.jpg"
  thumbnail="https://example.com/thumb.jpg"
  alt="Example image"
></lazy-progressive-image>;

Vue

<template>
  <lazy-progressive-image
    src="https://example.com/full.jpg"
    thumbnail="https://example.com/thumb.jpg"
    alt="Example image"
    @image-loaded="onImageLoaded"
  />
</template>

<script setup>
function onImageLoaded(event) {
  const { src, type } = event.detail;
  console.log(`${type} image loaded:`, src);
}
</script>

Svelte

<lazy-progressive-image
  src="https://example.com/full.jpg"
  thumbnail="https://example.com/thumb.jpg"
  alt="Example image"
  on:image-loaded={(event) => {
    const { src, type } = event.detail;
    console.log(`${type} image loaded:`, src);
  }}
/>

Styling with CSS custom properties

The component exposes a small set of CSS custom properties so you can override its appearance from a parent page without reaching into its internal shadow DOM.

<style>
  lazy-progressive-image {
    --lpi-image-object-fit: cover;
    --lpi-image-filter: grayscale(1);
    --lpi-thumbnail-filter: contrast(1.2) brightness(1.2);
    --lpi-thumbnail-blur: 20px;
  }
</style>

Useful properties include:

  • --lpi-image-object-fit, and --lpi-image-filter for the full image
  • --lpi-thumbnail-opacity, --lpi-thumbnail-filter, and --lpi-thumbnail-blur for the thumbnail
  • --lpi-image-opacity, --lpi-image-transition, and --lpi-thumbnail-transition for transition behavior

The styles for the lazy-progressive-image element itself can be overridden with normal CSS properties:

lazy-progressive-image {
  display: block;
  box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.15);
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

Note: the component needs a definite height to have somewhere to render the thumbnail placeholder before any image has loaded (the default :host sizing is a fixed 300px × 200px). When overriding size from the consumer, set an explicit height or an aspect-ratio as shown above — height: auto alone will collapse to 0px until the full image loads, since the placeholder is positioned absolutely and can't establish its own height.

Examples

Example projects for each framework live under examples/:

examples/
├── vanilla/
│   └── index.html
├── react/
│   ├── App.tsx
│   └── package.json
├── vue/
│   └── App.vue
└── svelte/
    └── App.svelte

To run the vanilla example:

cd examples/vanilla
npx serve .

To run a framework example, follow the package.json scripts inside each directory.

API

Web component attributes

  • src?: string — full-size image URL
  • thumbnail?: string — blurred thumbnail URL
  • alt?: string — image alt text
  • root-margin?: stringIntersectionObserver root margin (default: "100px")

React props

  • src?: string
  • thumbnail?: string
  • alt?: string
  • rootMargin?: stringIntersectionObserver root margin (default: "100px")
  • className?: string
  • style?: React.CSSProperties
  • onLoad?: (event: Event) => void
  • onError?: (event: Event) => void

Local Development

pnpm install
pnpm test
pnpm build

Tests

This project uses two test runners:

  • Unit tests — Vitest with happy-dom for fast logic tests.
  • Browser tests@web/test-runner with Playwright for real browser behavior.
pnpm run test:unit     # Vitest
pnpm run test:browser  # Playwright/Chromium
pnpm test              # both

Publish in a New Repo

  1. Copy the repository contents into a new git repository root.
  2. Optionally rename the package in package.json (name, repository, author).
  3. Run pnpm install.
  4. Build and test:
pnpm test
pnpm build
  1. Publish:
npm publish --access public

License

MIT