ComponentsOverlay
Lightbox
A full-screen image viewer with gallery navigation, keyboard and touch support. Built on Radix Dialog with image preloading and swipe gestures.
Gallery (Multiple Images)
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/ArrowRightto navigate,Escapeto 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 lightboxThis copies two files into your project:
components/ui/lightbox/lightbox.tsx— the componentcomponents/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
| Prop | Type | Default | Description |
|---|---|---|---|
images* | LightboxImage[] | — | Array of images to display. Each image requires a src URL and descriptive alt text. |
initialIndex | number | 0 | Zero-based index of the image to show when the lightbox first opens. |
open | boolean | — | Controlled open state. Use with onOpenChange for fully controlled usage. |
onOpenChange | (open: boolean) => void | — | Callback when the open state changes (user closes or Escape is pressed). |
children | React.ReactNode | — | Accepts LightboxTrigger and LightboxContent as children. |
Sub-components
| Component | Element | Purpose |
|---|---|---|
Lightbox | Root | Context provider with image data and navigation state |
LightboxTrigger | <button> | Opens the lightbox; use asChild to wrap a custom trigger |
LightboxContent | Dialog | Full-screen image viewer with navigation controls |
Accessibility
- Built on Radix UI Dialog —
role="dialog",aria-modal="true", visually hidden title and description are provided automatically. aria-labelon the content is set dynamically to the current image'salttext.- 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
alttext for every image — it is used for accessibility announcements.