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

react-motion-lightbox

v0.9.1

Published

A React lightbox and image gallery that opens out of the photo you click and closes back into it: swipe, pinch and wheel zoom, thumbnails, video, focus-point crops. Built on Motion.

Readme

A lightbox for React. It opens out of the thumbnail you clicked and closes back into it. Building a phone app? react-native-motion-lightbox does the same in React Native, with the same props.

I built it for my own site, gullberg.no, and pulled it out into a package so Gryt can use it too.

https://github.com/user-attachments/assets/6dc8bf07-384d-446f-8ed4-877b0d18ccab

What it does

  • Opens out of the thumbnail when you tell it which one. Without one, it fades in.
  • You can drag the pictures, flick them, and grab one while it's still moving.
  • Zoom with a click or tap on the picture, the scroll wheel, two fingers, or + and -. A click zooms 2.5x around where you clicked, and another zooms back out. It goes up to 8x.
  • Esc and the arrow keys work, and so does swiping. While zoomed in, the arrow keys move around the picture.
  • Pull the picture down to close it. It follows your finger and the page shows through; let go far enough or fast enough and it goes back into its thumbnail from there.
  • Nothing responds while the picture is moving in or out.
  • Click the empty space around the picture to close it. Move on with the arrows, the arrow keys, a swipe or the thumbnails.
  • It's a native <dialog>, so focus stays inside while it's open. When it closes, focus goes back to where it was.
  • It loads the next and previous picture before you get there.
  • With reduced motion turned on, nothing slides. It switches straight away.
  • There's no icon library and no CSS framework. You get one stylesheet you can theme with CSS variables.

You can draw videos with your own player, and put your own buttons and text at the top.

There's also a small one for the page, Gallery. It shows one picture at a time in a box, and opens the lightbox when you click it.

For React Native there's react-native-motion-lightbox, with the same props wherever they make sense.

Install

npm install react-motion-lightbox motion

You need react and react-dom 18 or newer, and motion 12 or newer.

Example

import { useRef, useState } from 'react'
import { Lightbox, type LightboxItem } from 'react-motion-lightbox'
import 'react-motion-lightbox/styles.css'

const items: LightboxItem[] = [
  { kind: 'image', src: '/photos/one.jpg', alt: 'The harbour at dawn', width: 1600, height: 1067 },
  { kind: 'image', src: '/photos/two.jpg', alt: 'A boat leaving' },
  { kind: 'video', src: '/clips/three.mp4', poster: '/clips/three.jpg', width: 1920, height: 1080 },
]

export function Gallery() {
  const thumbs = useRef<(HTMLImageElement | null)[]>([])
  const [open, setOpen] = useState(false)
  const [index, setIndex] = useState(0)

  return (
    <>
      {items.map((item, i) => (
        <button key={i} onClick={() => { setIndex(i); setOpen(true) }}>
          <img ref={(el) => { thumbs.current[i] = el }} src={item.poster ?? item.src} alt={item.alt} />
        </button>
      ))}

      <Lightbox
        items={items}
        index={index}
        open={open}
        onClose={() => setOpen(false)}
        onIndexChange={setIndex}
        origin={(i) => thumbs.current[i]}
        caption={(item) => item.alt}
        toolbar={(item) => <a href={item.src} download>Download</a>}
      />
    </>
  )
}

Gallery

import { Gallery } from 'react-motion-lightbox'

<Gallery
  items={[
    { kind: 'image', src: '/photos/fjord.jpg', width: 5200, height: 3464, focus: [0.78, 0.45], zoom: 1.6 },
    { kind: 'image', src: '/photos/harbour.jpg', width: 6000, height: 4000 },
  ]}
  caption={(item) => item.alt}
/>

By default each picture fills the box. focus is the point the box centres on, as fractions of the picture. zoom is how far it zooms in on that point. Leave them out and you get the middle.

Drag it, flick it, or use the arrows. Click the picture and the lightbox opens out of exactly what was in the box, then shows the whole picture.

It takes the same props as the lightbox and passes them on. On top of those:

| Prop | | |---|---| | aspect | The box's width over its height. Default 1, a square. | | loading | 'lazy' (the default) loads the pictures once the gallery is within half a screen of being seen. 'eager' loads them straight away: use it for a gallery at the top of the page. | | autoplay | Move on by itself every this many milliseconds, like 6000. It holds while the pointer or focus is on the gallery or the lightbox is open, adds a pause button, and stays still with reduced motion. | | fit | 'cover' (the default) fills the box with the part focus and zoom pick. 'contain' shows the whole picture inside the box. | | peek | How many pixels of the next picture show at the right edge, so it reads as something to swipe. Default 0. | | gap | The space between pictures, in pixels. Default 48. | | onLightboxChange(state) | 'open', 'closing' or 'closed'. Use it to move things on the page out of the lightbox's way. | | onPress(index) | Called on a click instead of opening the lightbox, if your app has its own viewer. A drag never counts as a click. | | renderSlide(item, index) | Draw what's in a slide yourself, in place of its picture: if your app loads pictures its own way or puts a menu on them. The slide stays the button that opens the lightbox or calls onPress. | | clip | Cut pictures off at the box's edges as they slide. Default true. Set it to false and they slide past the edges and fade out instead. | | fade | With clip={false}, whether pictures fade as they leave the box. Default true. Set it to false and they stay fully visible on either side, like a carousel. Whatever the gallery sits in decides how far out they show. |

className goes on both the gallery and the lightbox. A video shows its poster, or your own player if you pass renderVideo. active is true for the one on screen. If you build a gallery of your own, cropOf(item, aspect) gives you the same crop.

Props

| Prop | | |---|---| | items | { kind: 'image' \| 'video', src, srcSet?, sizes?, alt?, width?, height?, poster?, thumb?, focus?, zoom? }[]. If you leave out width and height, it measures the item once it loads. The lightbox shows each item at most at this size, so a small picture isn't blown up. | | index, open | Which item to show, and whether the lightbox is open. Change index while it's open and it moves there. | | onClose | Someone pressed Esc or the close button. Set open to false and it animates out. | | onIndexChange(index) | The item on screen changed. | | onExited(index) | The close animation is done and the dialog is gone. | | origin(index) | The element (or a DOMRect) an item opens out of and closes back into. Return nothing and it fades instead. | | originCrop(index) | If the thumbnail only shows part of the picture: [left, top, width, height] as fractions of it. | | renderVideo(item, { active }) | Draw a video yourself. It fills the item's box. | | title(item, index) | Top left. | | toolbar(item, index) | Top right, before the close button. | | caption(item, index) | Under the picture, before the "3 / 12" count. | | thumbnails | The row of thumbnails when there's more than one item. Default true. | | maxZoom | Default 8. | | radius | Corner radius in px. It stays the same all the way through the opening move. Default 12. | | labels | { dialog, close, previous, next, pause, play }, if you need other words or another language. pause and play are the gallery's slideshow button. | | icons | { close, previous, next, pause, play }, any React node. The defaults are plain SVGs. | | className | Goes on the <dialog>. |

preload(item) downloads an item the same way the lightbox will show it. Call it on hover and the picture is there before the click.

useTrack and useTrackPlace are the track the lightbox moves on. Use them if you want a gallery that moves the same way.

Theming

Set these on .rml, or on a class you pass in className:

.rml.my-lightbox {
  --rml-backdrop: rgb(0 0 0 / 0.92);
  --rml-blur: 0px;
  --rml-color: #f4f4f5;
  --rml-muted: #a1a1aa;
  --rml-border: rgb(255 255 255 / 0.2);
  --rml-accent: #e4c590;
  --rml-focus: #fde68a;
  --rml-padding: 1rem clamp(1rem, 4vw, 3rem);
  --rml-gap: 0.75rem;
  --rml-button-size: 2.75rem;
  --rml-caption-size: 0.75rem;
  --rml-thumb-size: 3.5rem;
  --rml-thumb-radius: 0.375rem;
  --rml-frame: #e0b04a;
  --rml-frame-width: 2px;
  --rml-ease: cubic-bezier(0.16, 1, 0.3, 1);
}

--rml-blur blurs the page behind. The blur and the backdrop come in and go out with the picture as it opens and closes. --rml-frame is the frame around the current thumbnail.

The gallery uses the same variables, set on .rml-gallery. Its text and buttons take the page's colour unless you set them.

What I haven't tested

I've run it on React 19 with Motion 12 and 14. React 18 should work, but I haven't tried it. Pinch zoom is written for touch screens, but I've only tested it on a laptop so far.

License

MIT. The logo is Brown Mushroom from Microsoft's Fluent Emoji, also MIT (its licence).