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
npm install @reelkit/react-lightbox @reelkit/react lucide-react

Don't forget to import the styles:

typescript
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.

tsx
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)}
      />
    </>
  );
}

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
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:

tsx
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:

tsx
<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:

tsx
<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:

tsx
<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:

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
// 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:

tsx
<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.

tsx
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=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. 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.

tsx
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.

tsx
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.

tsx
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:

tsx
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.

tsx
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 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
interface LightboxItem {
  src: string;
  type?: 'image' | 'video';  // defaults to 'image'
  poster?: string;            // thumbnail for video items
  title?: string;
  description?: string;
  width?: number;
  height?: number;
}

ControlsRenderProps

typescript
interface ControlsRenderProps {
  item: LightboxItem;
  activeIndex: number;
  count: number;
  isFullscreen: boolean;
  onClose: () => void;
  onToggleFullscreen: () => void;
}
typescript
interface NavigationRenderProps {
  item: LightboxItem;
  activeIndex: number;
  count: number;
  onPrev: () => void;
  onNext: () => void;
}

SlideRenderProps

typescript
interface SlideRenderProps {
  item: LightboxItem;
  index: number;
  size: [number, number];
  isActive: boolean;
  onReady: () => void;
  onWaiting: () => void;
  onError: () => void;
}

InfoRenderProps

typescript
interface InfoRenderProps {
  item: LightboxItem;
  index: number;
}

Sub-Components

Reusable sub-components for composing custom controls via renderControls.

CloseButton

Default X close button.

tsx
import { CloseButton } from '@reelkit/react-lightbox';

<CloseButton onClick={onClose} />

Counter

Image counter pill showing "1 / 3".

tsx
import { Counter } from '@reelkit/react-lightbox';

<Counter currentIndex={activeIndex} count={count} />

FullscreenButton

Fullscreen toggle button (Maximize/Minimize icon).

tsx
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.

tsx
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
        muted={soundState.muted.value}
        onToggle={soundState.toggle}
      />
      <button onClick={onClose}>Close</button>
    </div>
  );
}

Hooks

useVideoSlideRenderer

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

typescript
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 video

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
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.

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
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.

tsx
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.

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
/* 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

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