@chrisader/react-lightbox
v0.2.0
Published
Composable React lightbox components with animated image viewing, zoom, and gallery navigation.
Maintainers
Readme
react-lightbox
Composable React lightbox primitives for galleries, zoom, and animated image transitions.
Install
npm install @chrisader/react-lightboxreact and react-dom are peer dependencies.
Quick start
Import the stylesheet once:
import "@chrisader/react-lightbox/styles.css";Wrap your gallery with PhotoProvider, then compose each lightbox with PhotoView:
import { PhotoProvider, PhotoView } from "@chrisader/react-lightbox";
export function Gallery() {
return (
<PhotoProvider>
<PhotoView.Backdrop />
<PhotoView>
<PhotoView.Thumbnail src="/thumb-1.jpg" alt="Preview" />
<PhotoView.Content>
<PhotoView.Image src="/image-1.jpg" alt="Full image" />
<PhotoView.Close />
<PhotoView.Previous />
<PhotoView.Next />
<PhotoView.Current />
<PhotoView.Total />
</PhotoView.Content>
</PhotoView>
</PhotoProvider>
);
}How it works
PhotoProviderstores gallery state and settings.PhotoView.Backdroprenders the shared overlay. Place it once as a direct child ofPhotoProvider, not insidePhotoView.- Each
PhotoViewregisters one item in that gallery. PhotoView.Thumbnailopens the matching lightbox view.PhotoView.Contentrenders the modal layer in a portal.PhotoView.Image,Close,Previous,Next,Current, andTotalare composable parts inside the modal.
Usage patterns
Multiple images in one gallery
Use one PhotoProvider around all related PhotoView items so navigation works across the full set.
import { PhotoProvider, PhotoView } from "@chrisader/react-lightbox";
const photos = [
{
id: "mountain",
thumb: "/thumbs/mountain.jpg",
full: "/photos/mountain.jpg",
alt: "Mountain lake",
},
{
id: "forest",
thumb: "/thumbs/forest.jpg",
full: "/photos/forest.jpg",
alt: "Forest trail",
},
{
id: "coast",
thumb: "/thumbs/coast.jpg",
full: "/photos/coast.jpg",
alt: "Rocky coast",
},
];
export function PhotoGrid() {
return (
<PhotoProvider>
<PhotoView.Backdrop />
<div className="grid">
{photos.map((photo) => (
<PhotoView key={photo.id} id={photo.id}>
<PhotoView.Thumbnail src={photo.thumb} alt={photo.alt} />
<PhotoView.Content>
<PhotoView.Image src={photo.full} alt={photo.alt} />
<PhotoView.Close />
<PhotoView.Previous />
<PhotoView.Next />
<div className="counter">
<PhotoView.Current />
<span> / </span>
<PhotoView.Total />
</div>
</PhotoView.Content>
</PhotoView>
))}
</div>
</PhotoProvider>
);
}Use the thumbnail as the full image
If you do not pass src to PhotoView.Image, it falls back to the thumbnail source for that view.
<PhotoProvider>
<PhotoView.Backdrop />
<PhotoView>
<PhotoView.Thumbnail src="/images/product.jpg" alt="Product photo" />
<PhotoView.Content>
<PhotoView.Image alt="Product photo" />
<PhotoView.Close />
</PhotoView.Content>
</PhotoView>
</PhotoProvider>Open the gallery from your own controls
Use usePhotoViewGallery anywhere inside PhotoProvider when you want buttons outside the thumbnails.
import {
PhotoProvider,
PhotoView,
usePhotoViewGallery,
} from "@chrisader/react-lightbox";
function GalleryToolbar() {
const { openGallery, close, currentIndex, totalImages, isOpen } =
usePhotoViewGallery();
return (
<div>
<button type="button" onClick={() => openGallery(0)}>
Open gallery
</button>
{isOpen ? (
<button type="button" onClick={close}>
Close
</button>
) : null}
<span>
{isOpen ? currentIndex + 1 : 0} / {totalImages}
</span>
</div>
);
}
export function Gallery() {
return (
<PhotoProvider>
<PhotoView.Backdrop />
<GalleryToolbar />
<PhotoView id="one">
<PhotoView.Thumbnail src="/thumb-1.jpg" alt="Photo 1" />
<PhotoView.Content>
<PhotoView.Image src="/image-1.jpg" alt="Photo 1" />
</PhotoView.Content>
</PhotoView>
<PhotoView id="two">
<PhotoView.Thumbnail src="/thumb-2.jpg" alt="Photo 2" />
<PhotoView.Content>
<PhotoView.Image src="/image-2.jpg" alt="Photo 2" />
</PhotoView.Content>
</PhotoView>
</PhotoProvider>
);
}Replace icons and labels
Button parts accept children, and label parts accept a render function.
<PhotoView.Content>
<PhotoView.Image src="/image-1.jpg" alt="Studio shot" />
<PhotoView.Close>Close</PhotoView.Close>
<PhotoView.Previous>Back</PhotoView.Previous>
<PhotoView.Next>Forward</PhotoView.Next>
<PhotoView.Current
render={(current, total) => `Image ${current} of ${total}`}
/>
</PhotoView.Content>Bring your own styles
Every visual part accepts className and style. If you want to remove the default package classes, pass resetStyles.
<PhotoView.Backdrop resetStyles className="lightbox-backdrop" />
<PhotoView.Content resetStyles className="lightbox-shell">
<PhotoView.Image
src="/image-1.jpg"
alt="Campaign photo"
resetStyles
className="lightbox-image"
/>
<PhotoView.Close resetStyles className="lightbox-close">
Close
</PhotoView.Close>
</PhotoView.Content>Compose a new design with Tailwind
If you want a fully custom layout, use resetStyles and provide the full structure with utility classes. In this pattern, the primitives still handle state, focus, gestures, and navigation.
import { PhotoProvider, PhotoView } from "@chrisader/react-lightbox";
const photos = [
{
id: "01",
thumb: "/thumb-1.jpg",
full: "/image-1.jpg",
alt: "Editorial portrait",
},
{
id: "02",
thumb: "/thumb-2.jpg",
full: "/image-2.jpg",
alt: "Studio table scene",
},
{
id: "03",
thumb: "/thumb-3.jpg",
full: "/image-3.jpg",
alt: "Concrete interior",
},
];
export function TailwindGallery() {
return (
<PhotoProvider settings={{ loop: true }}>
<PhotoView.Backdrop
resetStyles
className="fixed inset-0 z-[1001] bg-slate-950/90 backdrop-blur-md"
/>
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
{photos.map((photo) => (
<PhotoView key={photo.id} id={photo.id}>
<PhotoView.Thumbnail
src={photo.thumb}
alt={photo.alt}
resetStyles
className="aspect-[4/5] w-full rounded-2xl object-cover shadow-sm ring-1 ring-black/5 transition hover:scale-[1.01] hover:shadow-xl"
/>
<PhotoView.Content
resetStyles
className="fixed inset-0 z-[1001] flex items-center justify-center p-4 sm:p-8"
>
<div className="relative z-10 w-full max-w-6xl">
<div className="pointer-events-auto mb-4 flex items-center justify-between rounded-full border border-white/10 bg-white/10 px-4 py-3 text-sm text-white shadow-lg backdrop-blur">
<div className="flex items-center gap-2">
<PhotoView.Current resetStyles className="font-semibold" />
<span className="text-white/50">/</span>
<PhotoView.Total resetStyles className="text-white/70" />
</div>
<PhotoView.Close
resetStyles
className="inline-flex h-10 items-center rounded-full bg-white px-4 text-sm font-medium text-slate-900 transition hover:bg-slate-100"
>
Close
</PhotoView.Close>
</div>
<div className="relative overflow-hidden rounded-[28px] bg-black/30 shadow-2xl ring-1 ring-white/10">
<PhotoView.Image
src={photo.full}
alt={photo.alt}
resetStyles
className="max-h-[78vh] w-full object-contain select-none"
/>
<PhotoView.Previous
resetStyles
className="pointer-events-auto absolute left-3 top-1/2 inline-flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur transition hover:bg-white/20 disabled:opacity-40"
>
<span aria-hidden="true">Prev</span>
</PhotoView.Previous>
<PhotoView.Next
resetStyles
className="pointer-events-auto absolute right-3 top-1/2 inline-flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-white/10 text-white backdrop-blur transition hover:bg-white/20 disabled:opacity-40"
>
<span aria-hidden="true">Next</span>
</PhotoView.Next>
</div>
</div>
</PhotoView.Content>
</PhotoView>
))}
</div>
</PhotoProvider>
);
}You can skip the package stylesheet if every visible part in your design uses resetStyles.
Provider settings
Pass settings to PhotoProvider to tune behavior:
<PhotoProvider
settings={{
preloadCount: 2,
closeOnBackdropClick: true,
enableKeyboardNav: true,
enableTouchGestures: true,
animationDuration: 300,
minZoom: 1,
maxZoom: 4,
zoomLevels: [1, 1.5, 2.5],
loop: true,
}}
>
{children}
</PhotoProvider>Available settings:
preloadCount: Number of nearby images to preload.closeOnBackdropClick: Close when the backdrop is clicked.enableKeyboardNav: Enable Escape, Left Arrow, and Right Arrow shortcuts.enableTouchGestures: Enable swipe, pinch, and double tap gestures.animationDuration: Transition duration in milliseconds.minZoom: Minimum zoom level.maxZoom: Maximum zoom level.zoomLevels: Zoom steps used when toggling zoom.loop: Continue from last to first image during navigation.
Exports
Components:
PhotoProviderPhotoViewPhotoView.BackdropPhotoView.ThumbnailPhotoView.ContentPhotoView.ImagePhotoView.PreviousPhotoView.NextPhotoView.ClosePhotoView.CurrentPhotoView.TotalPhotoView.SpinnerPhotoView.Error
Hooks and utilities:
usePhotoViewGalleryusePhotoContextuseParentViewIdcn
Types:
PhotoProviderPropsPhotoViewPropsPhotoContextValueViewPhotoProviderSettingsPhotoViewThumbnailPropsPhotoViewImagePropsPhotoViewContentPropsPhotoViewButtonPropsPhotoViewLabelPropsZoomPanState
