Lightbox

A full-screen image and video gallery lightbox component using @reelkit/react-lightbox.

View live demo →

Features

Images & Video
Built-in video slide support
Touch Gestures
Swipe to navigate
Swipe to Close
Swipe up to dismiss
Keyboard Nav
Arrow keys + Escape
Fullscreen
Cross-browser API
Transitions
Slide, fade, flip, zoom-in
Preloading
Adjacent images prefetched
Sound Toggle
Per-slide mute/unmute
Loading States
Spinner + custom render
Error Handling
Error icon + custom render
Render Props
6 customizable render zones
Hooks
useVideoSlideRenderer + useFullscreen
URL State
Shareable, bookmarkable links

Installation

bash

Don't forget to import the styles:

typescript
Icons
The default controls use lucide-react for icons. If you prefer a different icon library, use renderControls and renderNavigation to provide your own.

Quick Start

The LightboxOverlay component displays images in fullscreen. Pass an array of LightboxItem objects and control visibility with a nullable index.

tsx

Live Demo

LightboxPage.tsx

Click a thumbnail to open the lightbox. Use arrow keys or swipe to navigate.

Video Slides (Opt-in)

Video support is fully opt-in and tree-shakeable — image-only usage pays zero extra bundle cost. Import useVideoSlideRenderer and wire its return values into LightboxOverlay. The hook handles loading states, sound management, and video lifecycle automatically.

tsx
How it works
  • The hook returns SoundProvider — wrap your overlay in it for mute/unmute to work
  • Videos autoplay (muted by default) when the slide becomes active
  • A shared video element is reused across slides for iOS sound continuity
  • Sound button appears automatically on video slides with a reactive mute toggle
  • Items without type: 'video' render as images (backward compatible)

Customization

Custom Controls

Use renderControls to replace the default close button, counter, and fullscreen toggle. Compose with the exported sub-components:

tsx

Custom Info Overlay

Use renderInfo to replace the default title/description gradient, or pass renderInfo={() => null} to hide it entirely:

tsx

Custom Navigation

Use renderNavigation to replace the default prev/next arrows:

tsx

Custom Slide

Use renderSlide for fully custom slide content. Return null to fall back to the default image slide:

tsx

Content Loading & Error Handling

The lightbox tracks per-slide loading and error states. A spinner shows while content loads; a broken-image icon shows for failed media. Errored URLs are cached so revisiting shows the error instantly without retrying.

Lifecycle Callbacks

When using renderSlide, call these callbacks to control the loading indicator:

CallbackWhen to call
onReadyImage loaded or video started playing. Clears loading and error states.
onWaitingVideo is buffering mid-playback. Shows the loading indicator.
onErrorContent failed to load. Shows error overlay and caches the URL as broken.
tsx

Custom Loading & Error UI

Replace the default spinner and error icon with custom components:

tsx

URL State

LightboxUrlOverlay is a separate component whose open state lives in the address bar. Build a controller with useOverlayUrlState from @reelkit/react and hand it over as controller: the gallery opens itself when the parameter names a slide and closes when it goes away. Links are shareable, and the back button closes the gallery instead of leaving the page.

Built-in keys
You can address slides with a built-in key — spread urlIndexKey (by position) or urlStableIdKey (by a stable id) into the controller — both re-exported from @reelkit/react. See the URL State guide and Core API.
tsx

The hook takes one options object and returns a UrlStateController (with set, index, value). Keep it for programmatic control: set is the low-level write the overlay uses internally (slide change, and set(null) to close). It drives the overlay programmatically too — set(index) opens it, the same as navigating to the parameter. Prefer a link for opening though: the href is shareable, opens in a new tab, and the back button closes it — all for free, with no handler.

Full useOverlayUrlState options (param, adapter, codec, locator): see the React API reference.

LightboxUrlOverlay itself takes just controller (required), an optional onClose, plus every visual and behavior prop LightboxOverlay takes (images, ariaLabel, transitionFn, the render props, and so on) — but no isOpen.

  • Opening costs one history entry. Paging slides replaces it, so a hundred swipes add none — one back step always leaves the gallery. Back closes; it does not step through photos.
  • A shared link like ?photo=3 opens the gallery at that slide. Closing a link that arrived with the page removes the parameter in place rather than navigating off your site.
  • Back closes only when you opened from within the app — the link pushed an entry, so back pops to the gallery. A shared link opened directly in a fresh tab has no history behind it, so browser-back leaves the site; the close button or Escape removes the parameter in place and keeps you on the gallery.
  • A parameter naming no slide — a stale bookmark, a hand-edited value — is dropped from the URL instead of leaving the address bar asserting a slide that cannot open.

In a routed app, pass an adapter. Writing history directly leaves the router's own location stale, and its next navigation drops the parameter.

tsx

Opening is a link. Because the open state lives in the URL, a thumbnail is an ordinary link — no click handler — and the browser's own behaviour comes free: open in a new tab, copy the address, preview on hover. In a routed app use the router's link so it stays client-side.

tsx

Prefer a stable identity for shareable links. The index is positional, so a bookmarked ?photo=3 opens a different image once the list is reordered. urlStableIdKey keys by each item's stable id, scanning the live list — one call covers the common case.

tsx

Pass hashCodec: base64UrlCodec to base64url-encode the id in the URL — reversible obfuscation, not a cryptographic hash.

Key by a different field (a slug), or page an infinite feed with locateAsync, and build the codec/locator yourself:

tsx

Infinite or paginated galleries. The sync locate can only answer for images already loaded — a shared link to image 400 of a feed that has loaded 20 comes up empty. locateAsync is the fallback, called only when locate misses.

Shortcut
Keying by the item’s id? Skip the hand-rolled codec and locator — pass locateAsync straight to urlStableIdKey({ items, locateAsync }) (it fetches on a miss, then returns the index). The fuller version below is for keying by another field, or for full control.
tsx
  • How you load is up to you — fetch contiguous pages up to the target, or fetch just that one image and append it. The URL keys by identity, not position, so findIndex returns wherever the item lands.
  • While locateAsync is pending the lightbox stays closed and the parameter is left alone, so the deep link survives the fetch. null or a rejection drops the parameter.
  • An answer arriving after the URL moved on, after a close, or after unmount is discarded — a slow fetch cannot open a slide nobody asked for.
  • Nothing is rendered while pending; the page already owns that loading state, so render your own skeleton.
  • There is no timeout — the lightbox cannot know how long the gallery is. Settle with null when pagination is exhausted, or the overlay stays closed indefinitely.
  • Whatever locateAsync returns is authoritative — the index of data it just fetched, taken as-is without re-reading images.

API Reference

LightboxOverlay Props

LightboxOverlayProps

PropTypeDefaultDescription
isOpenbooleanrequiredControls lightbox visibility. For URL-driven open state, reach for the separate LightboxUrlOverlay instead — see URL State below.
imagesLightboxItem[]requiredArray of images to display
ariaLabelstring'Image gallery'Accessible label for the dialog region; announced by screen readers when the lightbox opens
initialIndexnumber0Starting image index
transitionFnTransitionTransformFnslideTransitionSlide transition function. Import a built-in (slideTransition, flipTransition, lightboxFadeTransition, lightboxZoomTransition) or pass a custom one. Defaults to slideTransition when omitted.
apiRefMutableRefObject<ReelApi>-Ref to access Reel API
renderControls(props: ControlsRenderProps) => ReactNode-Custom controls, replaces default close button, counter, and fullscreen toggle
renderNavigation(props: NavigationRenderProps) => ReactNode-Custom navigation, replaces default prev/next arrows
renderInfo(props: InfoRenderProps) => ReactNode-Custom info overlay, replaces default title + description gradient. Return null to hide.
renderSlide(props: SlideRenderProps) => ReactNode | null-Custom slide rendering. Receives { item, index, size, isActive, onReady, onWaiting, onError }. Return null to fall back to default.
renderLoading(props: { item: LightboxItem; activeIndex: number }) => ReactNode-Custom loading indicator, replaces default spinner
renderError(props: { item: LightboxItem; activeIndex: number }) => ReactNode-Custom error indicator, replaces default error icon

LightboxUrlOverlay Props

LightboxUrlOverlayProps

Takes every visual and behaviour prop above except isOpen, and replaces it with a controller. initialIndex is ignored here — the controller's position picks the slide, so a value passed alongside it would be overwritten on every open.

PropTypeDefaultDescription
controllerUrlStateControllerrequiredController from useOverlayUrlState. Its position decides whether the overlay is open and which slide it shows; the overlay writes back through it on slide change and on close.

Callbacks

PropTypeDescription
onClose() => voidCalled when the lightbox closes. Required on LightboxOverlay (you own the open state, so you must handle closing); optional on LightboxUrlOverlay, where the URL drives closing — pass it only to react after close.
onSlideChange(index: number) => voidCalled after slide change

Reel Props (proxied)

These props are forwarded to the underlying Reel component.

PropTypeDefaultDescription
loopbooleanfalseEnable infinite loop
enableNavKeysbooleantrueEnable keyboard navigation
enableWheelbooleantrueEnable mouse wheel navigation
wheelDebounceMsnumber200Wheel debounce duration (ms)
transitionDurationnumber300Transition animation duration (ms)
swipeDistanceFactornumber0.12Swipe threshold (0-1)
swipeToCloseDirection'up' | 'down''up'Direction of the swipe-to-close gesture on mobile

Types

LightboxItem

typescript

ControlsRenderProps

typescript
typescript

SlideRenderProps

typescript

InfoRenderProps

typescript

Sub-Components

Reusable sub-components for composing custom controls via renderControls.

CloseButton

Default X close button.

tsx

Counter

Image counter pill showing "1 / 3".

tsx

FullscreenButton

Fullscreen toggle button (Maximize/Minimize icon).

tsx

SoundButton

Mute/unmute toggle button for video slides (Volume2/VolumeX icon). Included automatically in renderControls from useVideoSlideRenderer. For standalone use inside custom controls, access sound state via useSoundState.

tsx

Hooks

useVideoSlideRenderer

Hook for opt-in video support. Returns renderSlide, renderControls, and SoundProvider — wrap the overlay in SoundProvider and pass the render functions.

typescript

useFullscreen

Moved
useFullscreen was removed from @reelkit/react-lightbox. Import it from @reelkit/react instead.

Hook for managing fullscreen state with cross-browser support.

tsx

Transitions

Pass any TransitionTransformFn via the transitionFn prop. Importing only the transition you use lets the bundler tree-shake the rest. Defaults to slideTransition when omitted.

FunctionFromDescription
slideTransition@reelkit/react-lightboxStandard horizontal slide (default)
lightboxFadeTransition@reelkit/react-lightboxCrossfade between images
flipTransition@reelkit/react-lightbox3D card flip effect
lightboxZoomTransition@reelkit/react-lightboxZoom in from smaller to normal size
tsx

Custom Transition Function

Author your own TransitionTransformFn and pass it via transitionFn. The signature mirrors core slider transitions.

tsx

CSS Classes

All UI elements use plain CSS classes (not CSS modules) that can be targeted with higher-specificity selectors in a stylesheet loaded after @reelkit/react-lightbox/styles.css. For color, size, and z-index changes, prefer the CSS custom properties documented in the Theming section below.

ClassComponentDescription
.rk-lightbox-overlayOverlayRoot container (full-screen backdrop)
.rk-lightbox-spinnerOverlayDefault loading spinner
.rk-lightbox-img-errorOverlayError state container (broken image/video)
.rk-lightbox-img-error-textOverlayError state text label
.rk-lightbox-swipe-hintOverlayMobile swipe hint
.rk-lightbox-controls-leftControlsTop-left controls container
.rk-lightbox-btnControlsControl buttons (fullscreen, etc.)
.rk-lightbox-closeControlsClose button
.rk-lightbox-counterControlsImage counter chip
.rk-lightbox-navNavigationNavigation arrows (both)
.rk-lightbox-nav-prevNavigationPrevious arrow
.rk-lightbox-nav-nextNavigationNext arrow
.rk-lightbox-infoInfoTitle/description container
.rk-lightbox-titleInfoImage title
.rk-lightbox-descriptionInfoImage description
.rk-lightbox-slideSlideSlide container
.rk-lightbox-imgSlideImage element
.rk-lightbox-video-containerVideoSlideVideo slide container (opt-in)
.rk-lightbox-video-elementVideoSlideVideo element (opt-in)
.rk-lightbox-video-posterVideoSlideVideo poster image (opt-in)

Theming

Every color, size, z-index, and transition lives in a CSS custom property. Override one or many at :root (or any ancestor of the lightbox) to retheme without touching component source.

TokenDefaultControls
--rk-lightbox-overlay-bg#000Full-screen backdrop color
--rk-lightbox-overlay-z9999Overlay z-index
--rk-lightbox-top-shade-height80pxTop gradient scrim height
--rk-lightbox-top-shade-bglinear-gradient(rgba(0,0,0,0.6), transparent)Top gradient scrim color
--rk-lightbox-edge-padding16pxEdge inset for close / nav / top-left controls
--rk-lightbox-controls-gap12pxGap between top-left controls
--rk-lightbox-transition0.2sButton hover transition duration
--rk-lightbox-blur8pxBackdrop blur radius for buttons / chips
--rk-lightbox-btn-bgrgba(0, 0, 0, 0.5)Default background for close, nav, small buttons
--rk-lightbox-btn-bg-hoverrgba(255, 255, 255, 0.2)Hover background for close, nav, small buttons
--rk-lightbox-btn-fg#fffIcon color for close, nav, small buttons
--rk-lightbox-btn-size36pxSmall button size (fullscreen toggle, etc.)
--rk-lightbox-close-size40pxClose button size
--rk-lightbox-nav-size48pxPrev/next arrow size
--rk-lightbox-nav-opacity0.7Idle opacity of prev/next arrows
--rk-lightbox-counter-fg#fffCounter text color
--rk-lightbox-counter-bgrgba(0, 0, 0, 0.5)Counter chip background
--rk-lightbox-counter-size14pxCounter font size
--rk-lightbox-counter-padding6px 12pxCounter chip padding
--rk-lightbox-counter-radius20pxCounter chip border-radius
--rk-lightbox-spinner-size28pxDefault spinner width/height
--rk-lightbox-spinner-trackrgba(255, 255, 255, 0.2)Spinner track color
--rk-lightbox-spinner-fg#fffSpinner indicator color
--rk-lightbox-spinner-duration0.8sSpinner rotation duration
--rk-lightbox-error-fgrgba(255, 255, 255, 0.4)Error icon + text color
--rk-lightbox-error-text-size13pxError message font size
--rk-lightbox-info-bglinear-gradient(transparent, rgba(0,0,0,0.8))Caption scrim gradient
--rk-lightbox-info-padding24pxCaption inner padding
--rk-lightbox-title-size18pxTitle font size
--rk-lightbox-description-size14pxDescription font size
--rk-lightbox-info-fg#fffCaption text color
--rk-lightbox-hint-fgrgba(255, 255, 255, 0.5)Swipe hint text color
--rk-lightbox-hint-bgrgba(0, 0, 0, 0.3)Swipe hint chip background
--rk-lightbox-hint-duration3sSwipe hint fade-in/out total duration
--rk-lightbox-video-bg#000Letterbox background behind <video>

Drop the snippet below into a stylesheet loaded after @reelkit/react-lightbox/styles.css.

css

Accessibility

The overlay root is a modal dialog (role="dialog", aria-modal="true"). Set ariaLabel to change the screen-reader announcement; it defaults to "Image gallery". Each slide carries role="group", aria-roledescription="slide", and aria-label="Image N of M".

The lightbox captures focus on open and returns it to the trigger on close. Tab and Shift+Tab cycle through focusable elements inside; focus that escapes (click outside, programmatic focus) gets pulled back. Implemented with captureFocusForReturn and createFocusTrap from @reelkit/core.

Keyboard Shortcuts

KeyAction
ArrowLeftPrevious image
ArrowRightNext image
EscapeClose lightbox (or exit fullscreen if active)