Lightbox
A full-screen image and video gallery lightbox component using @reelkit/react-lightbox.
Features
Installation
npm install @reelkit/react-lightbox @reelkit/react lucide-reactDon't forget to import the styles:
import '@reelkit/react-lightbox/styles.css';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.
import { useState } from 'react';
import { LightboxOverlay, type LightboxItem } from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';
const images: LightboxItem[] = [
{
src: 'https://example.com/image1.jpg',
title: 'Sunset',
description: 'Beautiful sunset over the ocean',
},
{
src: 'https://example.com/image2.jpg',
title: 'Mountains',
},
];
function App() {
const [index, setIndex] = useState<number | null>(null);
return (
<>
{images.map((img, i) => (
<img
key={i}
src={img.src}
onClick={() => setIndex(i)}
/>
))}
<LightboxOverlay
isOpen={index !== null}
images={images}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
/>
</>
);
}import { useState } from 'react';
import {
LightboxOverlay,
flipTransition,
lightboxFadeTransition,
lightboxZoomTransition,
slideTransition,
type LightboxItem,
} from '@reelkit/react-lightbox';
import type { TransitionTransformFn } from '@reelkit/react';
import '@reelkit/react-lightbox/styles.css';
const images: LightboxItem[] = [
{
src: '/cdn/samples/images/image-01.jpg',
title: 'Mountain River',
description: 'A beautiful mountain river flowing through the forest',
width: 1600,
height: 1000,
},
{
src: '/cdn/samples/images/image-02.jpg',
title: 'Snowy Peaks',
description: 'Majestic snow-capped mountains reaching for the sky',
width: 1000,
height: 1600,
},
{
src: '/cdn/samples/images/image-03.jpg',
title: 'Foggy Forest',
description: 'Misty morning in the dense forest',
width: 1600,
height: 900,
},
{
src: '/cdn/samples/images/image-04.jpg',
title: 'Ocean Waves',
description: 'Powerful ocean waves crashing against the rocky shore',
width: 900,
height: 1400,
},
{
src: '/cdn/samples/images/image-05.jpg',
title: 'Autumn Path',
description: 'A winding path through the autumn forest',
width: 1600,
height: 1067,
},
{
src: '/cdn/samples/images/image-06.jpg',
title: 'Coastal Cliffs',
description: 'Dramatic coastal cliffs overlooking the deep blue sea',
width: 1600,
height: 1067,
},
];
const transitions: { label: string; fn: TransitionTransformFn }[] = [
{ label: 'slide', fn: slideTransition },
{ label: 'fade', fn: lightboxFadeTransition },
{ label: 'flip', fn: flipTransition },
{ label: 'zoom-in', fn: lightboxZoomTransition },
];
export default function App() {
const [index, setIndex] = useState<number | null>(null);
const [transitionFn, setTransitionFn] = useState<TransitionTransformFn>(
() => slideTransition,
);
return (
<div style={{ padding: 16, background: '#f8fafc', minHeight: '100vh' }}>
{/* Transition picker */}
<div style={{ display: 'flex', gap: 6, marginBottom: 12 }}>
{transitions.map((t) => (
<button
key={t.label}
onClick={() => setTransitionFn(() => t.fn)}
style={{
padding: '6px 14px', borderRadius: 8, border: 'none', cursor: 'pointer',
fontSize: 13, fontWeight: 500,
background: transitionFn === t.fn ? '#6366f1' : '#e2e8f0',
color: transitionFn === t.fn ? '#fff' : '#334155',
}}
>
{t.label}
</button>
))}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{images.map((img, i) => (
<button
key={i}
onClick={() => setIndex(i)}
style={{
position: 'relative', aspectRatio: '4 / 3', borderRadius: 8,
overflow: 'hidden', border: 'none', padding: 0, cursor: 'pointer',
background: '#e2e8f0',
}}
>
<img
src={img.src}
alt={img.title}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
</button>
))}
</div>
<LightboxOverlay
isOpen={index !== null}
images={images}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
transitionFn={transitionFn}
/>
</div>
);
}To play a gallery as a slideshow and save each photo's original, see the Gallery Slideshow recipe.
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.
import {
LightboxOverlay,
useVideoSlideRenderer,
type LightboxItem,
} from '@reelkit/react-lightbox';
import '@reelkit/react-lightbox/styles.css';
const items: LightboxItem[] = [
{ src: '/photo.jpg', title: 'Photo' },
{
src: '/clip.mp4',
type: 'video',
poster: '/clip-thumb.jpg',
title: 'Video Clip',
},
];
function Gallery() {
const [index, setIndex] = useState<number | null>(null);
const isOpen = index !== null;
const { renderSlide, renderControls, SoundProvider } =
useVideoSlideRenderer(items);
return (
<SoundProvider>
{/* thumbnails… */}
<LightboxOverlay
isOpen={isOpen}
images={items}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
renderSlide={renderSlide}
renderControls={renderControls}
/>
</SoundProvider>
);
}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:
import {
LightboxOverlay,
CloseButton,
Counter,
FullscreenButton,
} from '@reelkit/react-lightbox';
<LightboxOverlay
isOpen={isOpen}
images={images}
onClose={handleClose}
renderControls={({ onClose, activeIndex, count, isFullscreen, onToggleFullscreen }) => (
<div style={{ position: 'absolute', top: 12, left: 16, right: 16, display: 'flex', justifyContent: 'space-between', zIndex: 10 }}>
<Counter currentIndex={activeIndex} count={count} />
<div>
<FullscreenButton isFullscreen={isFullscreen} onToggle={onToggleFullscreen} />
<CloseButton onClick={onClose} />
</div>
</div>
)}
/>Custom Info Overlay
Use renderInfo to replace the default title/description gradient, or pass renderInfo={() => null} to hide it entirely:
<LightboxOverlay
isOpen={isOpen}
images={images}
onClose={handleClose}
renderInfo={({ item, index }) => (
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, padding: 16, background: 'linear-gradient(transparent, rgba(0,0,0,0.8))', color: '#fff', zIndex: 10 }}>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
)}
/>Custom Navigation
Use renderNavigation to replace the default prev/next arrows:
<LightboxOverlay
isOpen={isOpen}
images={images}
onClose={handleClose}
renderNavigation={({ onPrev, onNext, activeIndex, count }) => (
<div style={{ position: 'absolute', bottom: 24, left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: 12, zIndex: 10 }}>
<button onClick={onPrev} disabled={activeIndex === 0}>Prev</button>
<span>{activeIndex + 1} / {count}</span>
<button onClick={onNext} disabled={activeIndex === count - 1}>Next</button>
</div>
)}
/>Custom Slide
Use renderSlide for fully custom slide content. Return null to fall back to the default image slide:
<LightboxOverlay
isOpen={isOpen}
images={images}
onClose={handleClose}
renderSlide={({ item, index, size, isActive, onReady, onError }) => {
// Custom CTA on last slide
if (index === images.length - 1) {
return (
<div style={{ width: size[0], height: size[1], display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff' }}>
<h2>View all photos</h2>
</div>
);
}
return null; // default image slide
}}
/>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:
| Callback | When to call |
|---|---|
onReady | Image loaded or video started playing. Clears loading and error states. |
onWaiting | Video is buffering mid-playback. Shows the loading indicator. |
onError | Content failed to load. Shows error overlay and caches the URL as broken. |
// Inside renderSlide — wire callbacks to your custom media
renderSlide={({ item, index, size, isActive, onReady, onWaiting, onError }) => (
<div style={{ width: size[0], height: size[1] }}>
{item.type === 'video' ? (
<video
src={item.src}
poster={item.poster}
autoPlay={isActive}
onCanPlay={onReady}
onWaiting={onWaiting}
onError={onError}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
) : (
<img
src={item.src}
onLoad={onReady}
onError={onError}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
)}
</div>
)}Custom Loading & Error UI
Replace the default spinner and error icon with custom components:
<LightboxOverlay
isOpen={isOpen}
images={images}
onClose={() => setIsOpen(false)}
renderLoading={({ item, activeIndex }) => (
<div style={{
position: 'absolute', inset: 0, zIndex: 10,
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#fff', fontSize: 14,
}}>
Loading image {activeIndex + 1}...
</div>
)}
renderError={({ item, activeIndex }) => (
<div style={{
position: 'absolute', inset: 0, zIndex: 10,
display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center',
gap: 12, color: 'rgba(255,255,255,0.5)',
}}>
<span style={{ fontSize: 48 }}>!</span>
<span>Failed to load content</span>
</div>
)}
/>URL State
View live demo →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.
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/react';
import { LightboxUrlOverlay } from '@reelkit/react-lightbox';
import { Link } from 'react-router-dom';
const photo = useOverlayUrlState({
param: 'photo',
...urlIndexKey(() => images.length),
});
// Opening is a link — the href is the open action. No open flag, no handler:
// the overlay reads the URL and opens itself.
{images.map((image, i) => (
<Link key={image.src} to={`?photo=${i}`}>
<img src={image.src} />
</Link>
))}
<LightboxUrlOverlay controller={photo} images={images} />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=3opens 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. The packaged router adapters vouch for a same-page link push; a custom adapter that cannot say how the entry arrived gets the in-place close too, which leaves a duplicate of the page in history — one back step then looks like nothing happened, but nothing reopens.
- 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.
import { useReactRouterUrlAdapter } from '@reelkit/react/react-router-url-adapter';
const adapter = useReactRouterUrlAdapter();
const photo = useOverlayUrlState({
param: 'photo',
adapter,
...urlIndexKey(() => images.length),
});
<LightboxUrlOverlay controller={photo} images={images} />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.
import { Link } from 'react-router-dom';
// The href is the open action — no onClick, no open flag.
{images.map((image, i) => (
<Link key={image.src} to={`?photo=${i}`}>
<img src={image.src} />
</Link>
))}
<LightboxUrlOverlay controller={photo} images={images} />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.
const photo = useOverlayUrlState({
param: 'photo',
...urlStableIdKey({ items: () => images }),
});
<LightboxUrlOverlay controller={photo} images={images} />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:
const photo = useOverlayUrlState({
param: 'photo',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => images.findIndex((x) => x.slug === id),
identify: (index) => images[index].slug,
},
});
<LightboxUrlOverlay controller={photo} images={images} />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.
const photo = useOverlayUrlState({
param: 'photo',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => images.findIndex((x) => x.id === id),
identify: (index) => images[index].id,
locateAsync: async (id) => {
const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
if (!loaded) return null; // exhausted — link names no image
setImages(loaded); // commit — the overlay renders from this state
return loaded.findIndex((x) => x.id === id); // wherever it landed
},
},
});
<LightboxUrlOverlay controller={photo} images={images} />- 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
findIndexreturns wherever the item lands. - While
locateAsyncis pending the lightbox stays closed and the parameter is left alone, so the deep link survives the fetch.nullor 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
nullwhen pagination is exhausted, or the overlay stays closed indefinitely. - Whatever
locateAsyncreturns is authoritative — the index of data it just fetched, taken as-is without re-readingimages.
Auto-advance
Set autoAdvance to play the gallery as a slideshow when it opens. Off by default, so an existing lightbox renders and behaves as before.
import { LightboxOverlay } from '@reelkit/react-lightbox';
<LightboxOverlay
isOpen={index !== null}
images={images}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
autoAdvance
autoAdvanceInterval={4000}
/>;- Images stay for
autoAdvanceInterval(5000 ms by default), counted from the moment the image has loaded, so a slow image is never skipped before anyone saw it. An image that fails to load counts from its error. - Videos play once instead of looping and move on when they end. With
useVideoSlideRendererthis is wired for you; a customrenderSlidereadsautoAdvancefrom its props and reports the end throughonEnded. - It waits while the mouse rests on a control (a button, a link or the caption), during a swipe, and while the page is hidden, then continues from where it stopped.
- Moving by hand (arrows, keys, wheel, swipe) starts the new slide's interval from zero; the slideshow stays on.
- On the last slide it stops, unless
loopis set. The play/pause button in the controls shows the state and lets the viewer switch it.
A custom renderControls receives isAutoAdvancing and, only when autoAdvance is on, onToggleAutoAdvance — render a PlayPauseButton when it is there. The timing itself is createSlideshowController from @reelkit/core; see the Autoplay guide to build the same behaviour into your own player, and the Core API for its methods.
Download
Set showDownload to add a download button to the controls. It saves the active item from its downloadUrl, falling back to src, under its downloadName, falling back to the last segment of the URL.
import { LightboxOverlay, type LightboxItem } from '@reelkit/react-lightbox';
const images: LightboxItem[] = [
{
src: 'https://cdn.example.com/beach-1600.jpg',
// Save the original rather than the resized preview
downloadUrl: 'https://cdn.example.com/beach-original.jpg',
downloadName: 'beach.jpg',
},
];
<LightboxOverlay
isOpen={index !== null}
images={images}
initialIndex={index ?? 0}
onClose={() => setIndex(null)}
showDownload
/>;The file is fetched and saved, because a browser ignores the download attribute on a cross-origin link and would open the file instead. When the fetch fails — usually a server on another origin that sends no CORS headers — the file opens in a new tab, where the viewer can still save it. Serve the files with Access-Control-Allow-Origin for a one-click save. The button is disabled while a download is in progress.
A custom renderControls receives isDownloading and, only when showDownload is on, onDownload — render a DownloadButton when it is there. The helper is downloadFile from @reelkit/core; see the Download guide.
API Reference
LightboxOverlay Props
LightboxOverlayProps
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | required | Controls lightbox visibility. For URL-driven open state, reach for the separate LightboxUrlOverlay instead — see URL State below. |
images | LightboxItem[] | required | Array of images to display |
ariaLabel | string | 'Image gallery' | Accessible label for the dialog region; announced by screen readers when the lightbox opens |
initialIndex | number | 0 | Starting image index |
transitionFn | TransitionTransformFn | slideTransition | Slide transition function. Import a built-in (slideTransition, flipTransition, lightboxFadeTransition, lightboxZoomTransition) or pass a custom one. Defaults to slideTransition when omitted. |
autoAdvance | boolean | false | Play the gallery as a slideshow. See Auto-advance. |
autoAdvanceInterval | number | 5000 | How long each image stays during a slideshow, in milliseconds, counted once it has loaded |
showDownload | boolean | false | Add a download button for the active item. See Download. |
apiRef | MutableRefObject<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, autoAdvance, onReady, onWaiting, onError, onEnded }. 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.
| Prop | Type | Default | Description |
|---|---|---|---|
controller | UrlStateController | required | Controller 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
| Prop | Type | Description |
|---|---|---|
onClose | () => void | Called 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) => void | Called after slide change |
Reel Props (proxied)
These props are forwarded to the underlying Reel component. Their type is ReelProxyProps, the subset of the Reel props the overlay passes through; use it to type a wrapper that forwards them.
| Prop | Type | Default | Description |
|---|---|---|---|
loop | boolean | false | Enable infinite loop |
enableNavKeys | boolean | true | Enable keyboard navigation |
enableWheel | boolean | true | Enable mouse wheel navigation |
wheelDebounceMs | number | 200 | Wheel debounce duration (ms) |
transitionDuration | number | 300 | Transition animation duration (ms) |
swipeDistanceFactor | number | 0.12 | Swipe threshold (0-1) |
swipeToCloseDirection | 'up' | 'down' | 'up' | Direction of the swipe-to-close gesture on mobile |
Types
LightboxItem
interface LightboxItem {
src: string;
type?: 'image' | 'video'; // defaults to 'image'
poster?: string; // thumbnail for video items
title?: string;
description?: string;
width?: number;
height?: number;
downloadUrl?: string; // what the download button saves; defaults to src
downloadName?: string; // file name; defaults to the URL's last segment
}ControlsRenderProps
interface ControlsRenderProps {
item: LightboxItem;
activeIndex: number;
count: number;
isFullscreen: boolean;
isAutoAdvancing: boolean;
isDownloading: boolean;
onClose: () => void;
onToggleFullscreen: () => void;
onToggleAutoAdvance?: () => void; // only with autoAdvance
onDownload?: () => void; // only with showDownload
}NavigationRenderProps
interface NavigationRenderProps {
item: LightboxItem;
activeIndex: number;
count: number;
onPrev: () => void;
onNext: () => void;
}SlideRenderProps
interface SlideRenderProps {
item: LightboxItem;
index: number;
size: [number, number];
isActive: boolean;
autoAdvance: boolean; // a video should play once, not loop
onReady: () => void;
onWaiting: () => void;
onError: () => void;
onEnded: () => void; // a video played to its end
}InfoRenderProps
interface InfoRenderProps {
item: LightboxItem;
index: number;
}Sub-Components
Reusable sub-components for composing custom controls via renderControls.
CloseButton
Default X close button. Props (CloseButtonProps): onClick, optional className and style.
import { CloseButton } from '@reelkit/react-lightbox';
<CloseButton onClick={onClose} />Counter
Image counter pill showing "1 / 3". Props (CounterProps): currentIndex (zero-based), count, optional className and style.
import { Counter } from '@reelkit/react-lightbox';
<Counter currentIndex={activeIndex} count={count} />FullscreenButton
Fullscreen toggle button (Maximize/Minimize icon). Props (FullscreenButtonProps): isFullscreen, onToggle, optional className and style.
import { FullscreenButton } from '@reelkit/react-lightbox';
<FullscreenButton isFullscreen={isFullscreen} onToggle={onToggleFullscreen} />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. Props (SoundButtonProps): isMuted, onToggle, optional className and style.
import { SoundButton } from '@reelkit/react-lightbox';
import { useSoundState } from '@reelkit/react';
// Inside a component wrapped in SoundProvider:
function CustomControls({ onClose }) {
const soundState = useSoundState();
return (
<div>
<SoundButton
isMuted={soundState.muted.value}
onToggle={soundState.toggle}
/>
<button onClick={onClose}>Close</button>
</div>
);
}PlayPauseButton
Slideshow toggle (Play/Pause icon, aria-pressed while playing). Props (PlayPauseButtonProps): isPlaying, onToggle, optional className and style. The default controls show it when autoAdvance is on.
import {
LightboxOverlay,
PlayPauseButton,
CloseButton,
} from '@reelkit/react-lightbox';
<LightboxOverlay
autoAdvance
renderControls={({ isAutoAdvancing, onToggleAutoAdvance, onClose }) => (
<>
{onToggleAutoAdvance && (
<PlayPauseButton
isPlaying={isAutoAdvancing}
onToggle={onToggleAutoAdvance}
/>
)}
<CloseButton onClick={onClose} />
</>
)}
{...props}
/>;DownloadButton
Download button (Download icon), disabled while isDownloading. Props (DownloadButtonProps): isDownloading, onClick, optional className and style. The default controls show it when showDownload is on.
import {
LightboxOverlay,
DownloadButton,
CloseButton,
} from '@reelkit/react-lightbox';
<LightboxOverlay
showDownload
renderControls={({ isDownloading, onDownload, onClose }) => (
<>
{onDownload && (
<DownloadButton isDownloading={isDownloading} onClick={onDownload} />
)}
<CloseButton onClick={onClose} />
</>
)}
{...props}
/>;Hooks
useVideoSlideRenderer
Hook for opt-in video support. Returns renderSlide, renderControls, and SoundProvider — wrap the overlay in SoundProvider and pass the render functions. The return type is UseVideoSlideRendererResult, which also carries hasVideo, true when any item has type: 'video'.
Its renderSlide draws video items with LightboxVideoSlide, a video slide that plays through one shared <video> element, so sound keeps working on iOS, and remembers each video's position. Render it yourself only from a custom renderSlide. Its props (LightboxVideoSlideProps) are src, poster, isActive, size, slideKey (keys the saved position and captured frame), loop (default true) and the onPlaying, onWaiting, onError, onEnded callbacks.
import { useVideoSlideRenderer } from '@reelkit/react-lightbox';
const { renderSlide, renderControls, SoundProvider, hasVideo } =
useVideoSlideRenderer(items);
// SoundProvider — wrap LightboxOverlay in this for mute/unmute support
// renderSlide — pass to LightboxOverlay's renderSlide prop
// renderControls — pass to LightboxOverlay's renderControls prop
// (includes Counter, FullscreenButton, SoundButton, CloseButton)
// hasVideo — true if items contain at least one videouseFullscreen
Hook for managing fullscreen state with cross-browser support.
import { useRef } from 'react';
import { useFullscreen } from '@reelkit/react';
function CustomLightbox() {
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, requestFullscreen, exitFullscreen, toggleFullscreen] =
useFullscreen({ ref: containerRef });
return (
<div ref={containerRef}>
<button onClick={toggleFullscreen}>
{isFullscreen.value ? 'Exit Fullscreen' : 'Enter Fullscreen'}
</button>
</div>
);
}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.
| Function | From | Description |
|---|---|---|
slideTransition | @reelkit/react-lightbox | Standard horizontal slide (default) |
lightboxFadeTransition | @reelkit/react-lightbox | Crossfade between images |
flipTransition | @reelkit/react-lightbox | 3D card flip effect |
lightboxZoomTransition | @reelkit/react-lightbox | Zoom in from smaller to normal size |
import {
LightboxOverlay,
lightboxFadeTransition,
} from '@reelkit/react-lightbox';
<LightboxOverlay
isOpen={isOpen}
images={images}
initialIndex={0}
onClose={handleClose}
transitionFn={lightboxFadeTransition}
/>Custom Transition Function
Author your own TransitionTransformFn and pass it via transitionFn. The signature mirrors core slider transitions.
import { LightboxOverlay } from '@reelkit/react-lightbox';
import type { TransitionTransformFn } from '@reelkit/react';
const customFade: TransitionTransformFn = (offset, size) => ({
transform: `translate3d(${offset * size[0]}px, 0, 0)`,
opacity: 1 - Math.min(Math.abs(offset), 1),
});
<LightboxOverlay
isOpen={isOpen}
images={images}
transitionFn={customFade}
onClose={() => setIsOpen(false)}
/>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.
| Class | Component | Description |
|---|---|---|
.rk-lightbox-overlay | Overlay | Root container (full-screen backdrop) |
.rk-lightbox-spinner | Overlay | Default loading spinner |
.rk-lightbox-img-error | Overlay | Error state container (broken image/video) |
.rk-lightbox-img-error-text | Overlay | Error state text label |
.rk-lightbox-swipe-hint | Overlay | Mobile swipe hint |
.rk-lightbox-controls-left | Controls | Top-left controls container |
.rk-lightbox-btn | Controls | Control buttons (fullscreen, etc.) |
.rk-lightbox-play-pause | Controls | Slideshow play/pause button (with .rk-lightbox-btn) |
.rk-lightbox-download | Controls | Download button (with .rk-lightbox-btn) |
.rk-lightbox-close | Controls | Close button |
.rk-lightbox-counter | Controls | Image counter chip |
.rk-lightbox-nav | Navigation | Navigation arrows (both) |
.rk-lightbox-nav-prev | Navigation | Previous arrow |
.rk-lightbox-nav-next | Navigation | Next arrow |
.rk-lightbox-info | Info | Title/description container |
.rk-lightbox-title | Info | Image title |
.rk-lightbox-description | Info | Image description |
.rk-lightbox-slide | Slide | Slide container |
.rk-lightbox-img | Slide | Image element |
.rk-lightbox-video-container | VideoSlide | Video slide container (opt-in) |
.rk-lightbox-video-element | VideoSlide | Video element (opt-in) |
.rk-lightbox-video-poster | VideoSlide | Video 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.
| Token | Default | Controls |
|---|---|---|
--rk-lightbox-overlay-bg | #000 | Full-screen backdrop color |
--rk-lightbox-overlay-z | 9999 | Overlay z-index |
--rk-lightbox-top-shade-height | 80px | Top gradient scrim height |
--rk-lightbox-top-shade-bg | linear-gradient(rgba(0,0,0,0.6), transparent) | Top gradient scrim color |
--rk-lightbox-edge-padding | 16px | Edge inset for close / nav / top-left controls |
--rk-lightbox-controls-gap | 12px | Gap between top-left controls |
--rk-lightbox-transition | 0.2s | Button hover transition duration |
--rk-lightbox-blur | 8px | Backdrop blur radius for buttons / chips |
--rk-lightbox-btn-bg | rgba(0, 0, 0, 0.5) | Default background for close, nav, small buttons |
--rk-lightbox-btn-bg-hover | rgba(255, 255, 255, 0.2) | Hover background for close, nav, small buttons |
--rk-lightbox-btn-fg | #fff | Icon color for close, nav, small buttons |
--rk-lightbox-btn-size | 36px | Small button size (fullscreen toggle, etc.) |
--rk-lightbox-btn-disabled-opacity | 0.5 | Opacity of a disabled small button (download in progress) |
--rk-lightbox-close-size | 40px | Close button size |
--rk-lightbox-nav-size | 48px | Prev/next arrow size |
--rk-lightbox-nav-opacity | 0.7 | Idle opacity of prev/next arrows |
--rk-lightbox-counter-fg | #fff | Counter text color |
--rk-lightbox-counter-bg | rgba(0, 0, 0, 0.5) | Counter chip background |
--rk-lightbox-counter-size | 14px | Counter font size |
--rk-lightbox-counter-padding | 6px 12px | Counter chip padding |
--rk-lightbox-counter-radius | 20px | Counter chip border-radius |
--rk-lightbox-spinner-size | 28px | Default spinner width/height |
--rk-lightbox-spinner-track | rgba(255, 255, 255, 0.2) | Spinner track color |
--rk-lightbox-spinner-fg | #fff | Spinner indicator color |
--rk-lightbox-spinner-duration | 0.8s | Spinner rotation duration |
--rk-lightbox-error-fg | rgba(255, 255, 255, 0.4) | Error icon + text color |
--rk-lightbox-error-text-size | 13px | Error message font size |
--rk-lightbox-info-bg | linear-gradient(transparent, rgba(0,0,0,0.8)) | Caption scrim gradient |
--rk-lightbox-info-padding | 24px | Caption inner padding |
--rk-lightbox-title-size | 18px | Title font size |
--rk-lightbox-description-size | 14px | Description font size |
--rk-lightbox-info-fg | #fff | Caption text color |
--rk-lightbox-hint-fg | rgba(255, 255, 255, 0.5) | Swipe hint text color |
--rk-lightbox-hint-bg | rgba(0, 0, 0, 0.3) | Swipe hint chip background |
--rk-lightbox-hint-duration | 3s | Swipe hint fade-in/out total duration |
--rk-lightbox-video-bg | #000 | Letterbox background behind <video> |
Drop the snippet below into a stylesheet loaded after @reelkit/react-lightbox/styles.css.
/* Brand the lightbox */
:root {
--rk-lightbox-overlay-bg: #0f172a;
--rk-lightbox-btn-bg: rgba(99, 102, 241, 0.65);
--rk-lightbox-btn-bg-hover: rgba(168, 85, 247, 0.85);
--rk-lightbox-nav-size: 56px;
--rk-lightbox-counter-bg: rgba(99, 102, 241, 0.65);
--rk-lightbox-info-bg: linear-gradient(
transparent,
rgba(99, 102, 241, 0.55) 60%,
rgba(168, 85, 247, 0.85)
);
}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
| Key | Action |
|---|---|
ArrowLeft | Previous image |
ArrowRight | Next image |
Escape | Close lightbox (or exit fullscreen if active) |