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.