components / lightbox
Lightbox
Full-screen image viewer with navigation, counter and captions.
How to use
A modal `role="dialog"` rendered in a portal: focus is trapped, the page scroll is locked, Escape closes and ←/→ navigate (wrapping around). `open` and `index` can be controlled; without them it keeps its own state (`defaultOpen`, `defaultIndex`). Focus returns to the opener on close unless `onCloseAutoFocus` calls `preventDefault()`.
preview
Lightbox.tsx
import { useState } from "react";import { Lightbox } from "vaiperui"; const [open, setOpen] = useState(false); <button onClick={() => setOpen(true)}>Open</button><Lightbox open={open} onOpenChange={setOpen} items={[ { src: "/photos/road.jpg", alt: "Mountain road", caption: "Road" }, { src: "/photos/sky.jpg", alt: "Starry sky" }, ]}/>Default
preview
default.tsx
<Lightbox open={open} onOpenChange={setOpen} items={items} />Props
| Prop | Values | Default | Description |
|---|---|---|---|
| items | { src, alt, caption? }[] | — | Images to show |
| open / defaultOpen | boolean | false | Controlled / initial visibility |
| onOpenChange | (open: boolean) => void | none | Called when it asks to close |
| index / defaultIndex | number | 0 | Controlled / initial image |
| onIndexChange | (index: number) => void | none | Called when the image changes |
| label | string | Image viewer | Accessible name of the dialog |
| closeLabel / previousLabel / nextLabel | string | Close / Previous image / Next image | Accessible names of the buttons |
| counterLabel | (index, total) => string | 1 / 6 | Text of the counter |
| onCloseAutoFocus | (event: Event) => void | none | On close; preventDefault() skips returning the focus |
Accessibility
- The viewer is a `role="dialog"` with `aria-modal="true"`, named by `label`, rendered in a portal.
- On open, focus moves inside and Tab cycles inside the dialog (focus trap); the page scroll is locked while it is open.
- On close, focus returns to the element that had it before, unless `onCloseAutoFocus` calls `preventDefault()`.
- Close, previous and next are real buttons named by `closeLabel`, `previousLabel` and `nextLabel`; previous/next only exist with more than one image and wrap around.
- The image uses its `alt`, inside a `<figure>` with the `caption` as `<figcaption>`.
- The counter (`counterLabel`, e.g. `2 / 5`) is an `aria-live="polite"` region, so the change of image is announced.
Keyboard
| Key | Action |
|---|---|
| Esc | Closes the viewer |
| ArrowLeft / ArrowRight | Previous / next image (with more than one) |
| Tab / Shift+Tab | Cycles through the buttons inside the dialog |
Keep in mind
- Every item needs an `alt`; the component does not generate one.
- Clicking the dark backdrop also closes it, but that is a mouse shortcut; keyboard users close it with Esc or the close button. Translate the labels for the page language.