WebberUI

Lightbox Gallery (React)

A React thumbnail wall that opens a full-screen lightbox: the image morphs in from its thumbnail as a shared element, with wheel/pinch zoom and drag panning, double-tap zoom, swipe and keyboard navigation, a counter and caption bar, a bottom thumbnail strip, and Esc or backdrop to close.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

A thumbnail wall (CSS grid, gentle hover zoom, fixed 4:3 cells) — click any tile to open a full-screen lightbox: the large image scales up from the thumbnail's position as a shared element and shrinks back into place on close. Inside the lightbox the full gesture set is implemented with pointer events — mouse wheel or two-finger pinch zooms around the cursor / pinch midpoint, a single-finger drag pans once zoomed in (clamped to the image bounds), a horizontal swipe past 80px switches images at 1×, and a double-tap toggles between 1× and 2×. The toolbar carries the counter, zoom in/out, and close; arrow buttons and the ←/→ keys change images; a caption bar and a clickable thumbnail strip sit at the bottom. Opening locks page scroll, moves focus to the close button and cycles it inside the lightbox; closing returns focus to the original thumbnail.

Loading preview…
npx shadcn@latest add "https://webberui.com/r/lightbox-gallery.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

3
4
<LightboxGallery />

Installation

npx shadcn@latest add "https://webberui.com/r/lightbox-gallery.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/lightbox-gallery.

Usage

import { LightboxGallery, type LightboxImage } from "@/components/ui/lightbox-gallery";

const images: LightboxImage[] = [
  { id: "a", src: "/photos/a.jpg", thumb: "/photos/a-thumb.jpg", alt: "Sunrise at the shore", caption: "The east coast at 5 a.m." },
  { id: "b", src: "/photos/b.jpg", alt: "Sea of clouds in the valley" },
  { id: "c", src: "/photos/c.jpg", alt: "Old street in the afternoon", caption: "Light and shadow under the eaves" },
];

// Uncontrolled: click a thumbnail to open; the component manages open state and index
<LightboxGallery images={images} columns={3} loop />

// Controlled: open to a specific image from an external button
const [open, setOpen] = useState(false);
const [index, setIndex] = useState(0);
<LightboxGallery
  images={images}
  open={open}
  onOpenChange={setOpen}
  index={index}
  onIndexChange={setIndex}
  showThumbnails={false}
  zoomMax={6}
/>

Props

PropTypeDefaultDescription
imagesLightboxImage[]Image list; id must be unique
columnsnumber3Number of columns in the thumbnail wall
gapnumber8Gap between thumbnail cells (px)
startIndexnumber0Initial index in uncontrolled mode (the current image on first mount)
indexnumberControlled current index; managed internally when omitted
onIndexChange(index: number) => voidCallback when the current index changes (thumbnail click, navigation, and swipe all fire it)
openbooleanControlled open state of the lightbox; managed internally when omitted
defaultOpenbooleanfalseInitial open state in uncontrolled mode
onOpenChange(open: boolean) => voidCallback when the open state changes (fires in both controlled and uncontrolled mode)
loopbooleanfalseWhether navigation wraps around at both ends
showThumbnailsbooleantrueWhether to show the thumbnail strip at the bottom of the lightbox
showCounterbooleantrueWhether to show the "3 / 12" counter
zoomMaxnumber4Maximum zoom factor
labelsPartial<LightboxGalleryLabels>Custom UI strings (override only what you need)
classNamestringForwarded to the outermost container

LightboxImage

FieldTypeDefaultDescription
idstringUnique id; the shared-element animation pairs the thumbnail with the large image by it
srcstringFull-size source (needs an intrinsic size: any raster image, or an SVG with width/height)
altstringAlt text (required for accessibility); it is also announced by screen readers on navigation
captionstringText for the caption bar at the bottom of the lightbox
thumbstringThumbnail source; when omitted both the wall and the lightbox strip use src

LightboxGalleryLabels

FieldTypeDefaultDescription
dialogstring"圖片燈箱" (Image lightbox)aria-label of the lightbox dialog
openstring"放大檢視" (View larger)Suffix of each wall button's accessible name, forming "alt text (View larger)"
closestring"關閉" (Close)Close button
previousstring"上一張" (Previous)Previous button
nextstring"下一張" (Next)Next button
zoomInstring"放大" (Zoom in)Zoom-in button
zoomOutstring"縮小" (Zoom out)Zoom-out button
thumbnailsstring"縮圖列" (Thumbnails)aria-label of the thumbnail strip at the bottom of the lightbox

The LightboxImage, LightboxGalleryLabels, and LightboxGalleryProps types are also named exports.

How it works

  • Shared-element entrance: the image the lightbox opens with shares a Motion layoutId with its thumbnail, so it scales up from the thumbnail's position; if you close while still on that image it shrinks back into the thumbnail, and after navigating to another image it fades out instead. Images navigated to afterwards slide in and out horizontally, and zoom/pan reset on every switch.
  • Gestures: implemented with pointer events, so mouse, touch, and pen behave the same. The wheel (including trackpad pinch, which arrives as a wheel event with ctrlKey) zooms around the cursor; a two-finger pinch tracks the distance and midpoint of two pointers, so you can pan while pinching; above 1× a single-finger drag pans, clamped to the image bounds; at 1× a horizontal displacement past 80px switches images (with resistance and a spring back at the ends when loop is off); a second tap within 300ms and 24px counts as a double-tap and toggles between 1× and 2×; tapping the backdrop outside the image closes.
  • Keyboard: ←/→ to navigate, Home/End to jump to the ends, Esc to close, +/- to zoom, 0 to reset to 1×.
  • Sizing: the large image is fitted inside the stage with max-width: 100%; max-height: 100%, so images smaller than the stage are not upscaled; src needs an intrinsic size (all raster formats have one; give SVGs a width/height). The previous and next images are preloaded while open.
  • Portal and scroll lock: the lightbox is rendered into document.body via createPortal so ancestor transform/overflow cannot clip it; while open, an effect sets document.body.style.overflow to hidden and restores the previous value on close or unmount.

Accessibility

  • The lightbox is role="dialog" with aria-modal="true"; its aria-label comes from labels.dialog, and when a caption exists aria-describedby points at the caption bar. Focus moves to the close button on open, Tab/Shift+Tab cycle inside the lightbox, and focus returns to the clicked thumbnail on close
  • Every wall cell is a button (aria-haspopup="dialog") whose accessible name is "alt text (View larger)"; toolbar, arrow, and strip buttons all carry aria-label, the current thumbnail is marked with aria-current, and an aria-live="polite" region announces "image n of N: alt text" on navigation
  • Every interaction has a keyboard equivalent (arrow keys to navigate, Esc to close, +/-/0 to zoom), and buttons show a focus-visible outline
  • When the user has "reduce motion" enabled at the system level, the shared-element morph becomes a fade, the horizontal slide becomes a fade, zoom snaps to the target without tweening, and the thumbnail strip scrolls instantly — nothing functional is lost

On this page