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

blurst

v0.1.0

Published

Instant image lightbox for React. Opens on tap with a blurred preview, bursts to full resolution, pinch to zoom, swipe to dismiss.

Readme

blurst

The React image lightbox that opens on the first frame.

npm bundle size dependencies license

Live demo and playground · npm


Tap an image and the viewer is already open — showing the thumbnail the browser has cached, blurred, until the full-resolution file decodes and takes its place. No spinner on black, no empty frame, no layout jump.

npm i blurst
import { BlurstImage } from "blurst";

<BlurstImage src="/photo-thumb.jpg" full="/photo-full.jpg" alt="Lysefjord" />;

That is the whole setup. No CSS import, no provider, no dependencies.

Why

Most lightboxes mount an empty overlay and then start downloading, so a tap on a slow connection gives you a spinner on black. Blurst treats the thumbnail already painted on the page as the first frame of the full image:

| | | |---|---| | 0 ms to first paint | The already-decoded thumbnail is the placeholder. Nothing is fetched to open. | | 7 kB gzipped | One file, zero dependencies, no stylesheet to import. | | Native gestures | Pinch, double-tap, pan, swipe to dismiss, swipe between images. | | Any image | A 32 px avatar, a grid tile, a full-bleed hero, or markup you don't own. | | Accessible | Labelled modal dialog, full keyboard control, honours prefers-reduced-motion. | | SSR-safe | Ships its own "use client" boundary; drops into the Next.js App Router. |

Setups

import { BlurstImage } from "blurst";

<BlurstImage
  src="/photo-thumb.jpg"
  full="/photo-full.jpg"
  alt="Lysefjord, Norway"
  caption="Lysefjord, Norway"
  className="rounded-lg"
/>;

Every <img> prop still works — srcset, sizes, loading, className, onLoad.

import { BlurstGroup, BlurstImage } from "blurst";

<BlurstGroup>
  {photos.map((p) => (
    <BlurstImage key={p.id} src={p.thumb} full={p.full} alt={p.alt} caption={p.caption} />
  ))}
</BlurstGroup>;

Triggers inside a group are ordered by their position in the document. Swipe sideways, use the arrow keys, or click the chevrons.

import { Blurst } from "blurst";

<Blurst src="/receipt-full.jpg" thumb="/receipt-thumb.jpg" caption="Invoice 4021">
  <button className="card">…</button>
</Blurst>;

Wraps a single child — a card, a CSS background, a next/image, a canvas.

import { BlurstAuto } from "blurst";

<article dangerouslySetInnerHTML={{ __html: post.html }} />
<BlurstAuto selector="article img" gallery />

Attaches by CSS selector to images already in the DOM. Nothing about their markup changes. Point full at a data-blurst attribute, an enclosing image link, or let it take the largest candidate from srcset.

import { BlurstProvider, useBlurst } from "blurst";

// once, near the root
<BlurstProvider options={{ blur: 20 }}>{children}</BlurstProvider>;

// anywhere below it
const { open } = useBlurst();
open({ items: photos, index: 2 });

Where the full-resolution image comes from

Pass full and it is used. Otherwise Blurst resolves it, in order:

  1. a data-blurst / data-blurst-full attribute on the image
  2. an enclosing <a> whose href points at an image file
  3. the largest candidate in srcset
  4. the image's own source — the viewer then opens sharp with no second request

Downloading starts on pointer or touch intent, a beat before the click lands. Set preload: "open" to wait for the tap, or preload: "eager" to fetch on idle. Override the whole thing with resolveFull: (el) => string.

Options

Set them per image, per group, or once on BlurstProvider. The nearest wins.

| Option | Default | | |---|---|---| | transition | "zoom" | Flies from the thumbnail, "fade", or "none" | | openDuration / closeDuration | 260 / 200 | ms | | blur | 16 | Placeholder blur radius, px | | backdrop | "rgba(0,0,0,0.92)" | Backdrop colour | | backdropBlur | 0 | backdrop-filter blur, px | | padding | 24 | Gap to the viewport edge, px | | preload | "hover" | "hover" · "open" · "eager" | | resolveFull | — | Your own trigger → URL function | | swipeToDismiss | true | Drag the image away to close | | swipeAxis | "any" | Restrict the dismiss drag to one axis | | dismissThreshold | 96 | Drag distance that commits to a close, px | | pinchToZoom | true | Two-finger pinch, anchored at the midpoint | | doubleTapToZoom | true | Double tap or click zooms at that point | | wheelToZoom | true | Trackpad pinch and ctrl+wheel | | maxScale / doubleTapScale | 5 / 2.5 | Zoom bounds | | swipeToNavigate | true | Horizontal swipe moves through a group | | loop | true | Wrap at the ends of a group | | closeOnBackdropClick | true | | | keyboard | true | Escape, arrows, +, -, 0 | | closeButton / counter / captions | true | Chrome toggles | | loader | — | Custom spinner, or false | | zIndex | 9999 | | | container | document.body | Portal target | | ariaLabel | "Image viewer" | | | onOpen / onClose / onIndexChange / onZoom | — | Callbacks |

Keyboard and accessibility

The overlay is a labelled role="dialog" with aria-modal. Escape closes, ←/→ move through a group, +, - and 0 control zoom. Body scroll is locked while open, with scrollbar-width compensation. prefers-reduced-motion: reduce turns off every transition.

Requirements

React 18 or 19, and react-dom. Both are peer dependencies. TypeScript types are bundled.

License

MIT © Cameron Clark