Lightbox

API reference

View as Markdown

Every part, prop and hook. Parts accept all props of their underlying element, plus Base UI's render prop.

import { Lightbox, useLightbox } from '@frontendsupply/lightbox'

Root

Holds state. Renders no element.

Prop Type Default
items LightboxItem[] — { src, alt?, width?, height?, srcSet?, sizes? } plus any fields you like
maxZoom number 4 Relative to the fitted size. 1 disables zoom
wheelToZoom boolean false Plain wheel zooms instead of panning. Ctrl/⌘ + wheel and pinch always zoom
closeOnBackdropClick boolean true
scrollTriggerIntoView 'smooth' | 'instant' | false 'smooth' Keep the current item’s trigger in view behind the viewer
transition Motion Transition spring The open / close morph
onOpenChange (open: boolean) => void
onIndexChange (index: number) => void

Trigger

A <button> that opens the viewer at index. Its first <img> is the morph source; its own box, radius and any clipping ancestors (scrollers, overflow: hidden cards) define the visible crop. While its item is open that <img> is hidden, so the slot reads as “lifted out”; style [data-active] for anything else in the trigger (e.g. a “+3” overlay). Several triggers may share an index (e.g. a hero grid and an all-photos grid); the most visible one wins.

Prop Type
index number Item to open
render ReactElement | (props, state) => ReactElement Render as another element, e.g. <a />

Portal, Backdrop, Popup

Base UI Dialog parts. Popup traps focus, closes on Esc, returns focus to the current item’s trigger and handles the keyboard shortcuts (your own onKeyDown runs first; call preventDefault() to replace a shortcut). Page scroll is locked without reserving a scrollbar gutter, so the overlay is truly full-bleed and nothing behind it shifts. Backdrop and Popup get --lightbox-presence; Backdrop’s opacity follows it.

Viewport

The horizontal scroll-snap scroller. With no children it renders <Slide><Image /></Slide> for every item.

Slide

Prop Type
index number Item shown in this slide
onTap (event: LightboxTapEvent) => void Taps on the slide, after the double-tap window when zoom is on (instantly when maxZoom is 1)
interface LightboxTapEvent {
  index: number
  target: 'image' | 'backdrop'
  x: number // 0 → 1 across the image box
  y: number
  pointerType: string
  preventDefault(): void // skip the default: backdrop → close, touch on image → toggle data-ui-hidden
}

Only slides within two of the current index render their children. Non-current slides are inert. Slides are the scroll-snap targets, so don’t transform them; animate a child instead (see Styling).

Image

The morph and zoom target. Defaults src, alt, srcSet and sizes from the slide’s item; any <img> prop works, so a slide can swap what it shows (the Stories frames do exactly that). While the full image loads, the trigger’s thumbnail is shown in its place.

Close, Prev, Next, Title, Description

Buttons and labels. Prev / Next disable themselves at the ends.

Thumbnails, Thumbnail, ThumbnailIndicator

A thumbnail strip whose indicator travels with the swipe: halfway through a swipe it sits halfway between two thumbs, interpolating both position and size (thumbs may have different widths). The strip glides to keep it centred.

<Lightbox.Thumbnails className="flex gap-2 px-[50%]">
  {items.map((item, i) => (
    <Lightbox.Thumbnail key={item.src} index={i} className="h-11 opacity-50 data-active:opacity-100">
      <img src={item.thumb} alt="" className="size-full object-cover" />
    </Lightbox.Thumbnail>
  ))}
  <Lightbox.ThumbnailIndicator className="rounded-md outline-2 outline-offset-2 outline-white" />
</Lightbox.Thumbnails>

Thumbnails is the scroll container (any positioning works); Thumbnail is a button that goes to index and gets data-active / aria-current; ThumbnailIndicator is absolutely positioned and sized over the thumbs every frame — give it any look: a ring, an underline, a glow.

useLightbox()

Call it anywhere inside Root to build your own UI.

const {
  items, item, index, count,       // data
  phase, open, zoomed, dragging, uiHidden, // state
  progress,                        // MotionValue: fractional index in view (2.5 = halfway from 3rd to 4th)
  openAt, close, goTo, next, prev, // navigation
  zoomIn, zoomOut, resetZoom,      // zoom
} = useLightbox<MyItem>()

progress updates every frame of a swipe, drag or glide, so you can build your own dots, progress bars or parallax with Motion’s useTransform without re-rendering.