VisorVisor
ComponentsOverlay

Lightbox

A full-screen image viewer with gallery navigation, keyboard and touch support. Built on Radix Dialog with image preloading and swipe gestures.

Single Image

When only one image is provided, navigation arrows and the counter are hidden automatically.

Grid Thumbnails

A common pattern is to open the lightbox from a thumbnail grid, passing initialIndex to start at the clicked image.

import { useState } from 'react';
import { Lightbox, LightboxContent } from '@/components/ui/lightbox/lightbox';

const images = [
  { src: '/photo-1.jpg', alt: 'Mountain landscape' },
  { src: '/photo-2.jpg', alt: 'Ocean sunset' },
  { src: '/photo-3.jpg', alt: 'Forest trail' },
];

export default function ThumbnailGrid() {
  const [open, setOpen] = useState(false);
  const [index, setIndex] = useState(0);

  function handleClick(i: number) {
    setIndex(i);
    setOpen(true);
  }

  return (
    <>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '0.5rem' }}>
        {images.map((img, i) => (
          <button key={i} onClick={() => handleClick(i)} style={{ padding: 0, border: 'none', cursor: 'pointer' }}>
            <img src={img.src} alt={img.alt} style={{ width: '100%', display: 'block' }} />
          </button>
        ))}
      </div>
      <Lightbox images={images} open={open} onOpenChange={setOpen} initialIndex={index}>
        <LightboxContent />
      </Lightbox>
    </>
  );
}

Controlled State

Use open and onOpenChange to fully control the lightbox without a LightboxTrigger.

import { useState } from 'react';
import { Lightbox, LightboxContent } from '@/components/ui/lightbox/lightbox';
import { Button } from '@/components/ui/button/button';

export default function ControlledLightbox() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open Lightbox</Button>
      <Lightbox
        images={[{ src: '/photo.jpg', alt: 'A photo' }]}
        open={open}
        onOpenChange={setOpen}
      >
        <LightboxContent />
      </Lightbox>
    </>
  );
}

Features

  • Keyboard navigation: ArrowLeft/ArrowRight to navigate, Escape to close
  • Touch/swipe support: Swipe left/right on mobile to navigate
  • Image preloading: Adjacent images are preloaded for smooth transitions
  • Auto-hides navigation: Arrows and counter are hidden for single-image lightboxes
  • Accessible: Built on Radix Dialog for focus management and screen reader support
  • Theme-agnostic: Uses CSS custom properties for all visual tokens

Installation

npx visor add lightbox

This copies two files into your project:

  • components/ui/lightbox/lightbox.tsx — the component
  • components/ui/lightbox/lightbox.module.css — the styles

Usage

import {
  Lightbox,
  LightboxTrigger,
  LightboxContent,
} from '@/components/ui/lightbox/lightbox';

const images = [
  { src: '/photo-1.jpg', alt: 'Mountain landscape' },
  { src: '/photo-2.jpg', alt: 'Ocean sunset' },
  { src: '/photo-3.jpg', alt: 'Forest trail' },
];

export default function Example() {
  return (
    <Lightbox images={images}>
      <LightboxTrigger asChild>
        <Button>View Gallery</Button>
      </LightboxTrigger>
      <LightboxContent />
    </Lightbox>
  );
}

API Reference

PropTypeDefaultDescription
images*LightboxImage[]Array of images to display. Each image requires a src URL and descriptive alt text.
initialIndexnumber0Zero-based index of the image to show when the lightbox first opens.
openbooleanControlled open state. Use with onOpenChange for fully controlled usage.
onOpenChange(open: boolean) => voidCallback when the open state changes (user closes or Escape is pressed).
childrenReact.ReactNodeAccepts LightboxTrigger and LightboxContent as children.

Sub-components

ComponentElementPurpose
LightboxRootContext provider with image data and navigation state
LightboxTrigger<button>Opens the lightbox; use asChild to wrap a custom trigger
LightboxContentDialogFull-screen image viewer with navigation controls

Accessibility

  • Built on Radix UI Dialogrole="dialog", aria-modal="true", visually hidden title and description are provided automatically.
  • aria-label on the content is set dynamically to the current image's alt text.
  • The current position is announced as "Viewing image N of M" via a visually hidden description.
  • Navigation buttons have visually hidden "Previous image" / "Next image" labels.
  • The close button has a visually hidden "Close" label for screen readers.
  • Focus is trapped inside the lightbox when open and restored to the trigger on close.
  • Always provide meaningful alt text for every image — it is used for accessibility announcements.