# Lightbox

> Photos and videos one at a time, over everything, dark in both themes like a photo viewer, with your photo consent rule applied.

- Name: `lightbox`
- Status: beta
- Categories: media, overlays
- File: the install writes `lightbox.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`. The install adds them.
- Other registry items: `@arbokui/action-keys`, `@arbokui/button`, `@arbokui/icon`, `@arbokui/kbd`, `@arbokui/media-image`, `@arbokui/tooltip`. The install adds them.
- Gallery: https://arbokui.com/components/lightbox
- Registry item: https://arbokui.com/r/lightbox.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/lightbox
   ```

## Use it for

- Viewing a set of photos or videos large, one at a time.
- Open it from a photo's own button, and pass open and onOpenChange. It has no trigger of its own.
- Name the set with label, like 'Photos from the team offsite'. Screen readers hear it as it opens.
- Pass manage only to people who manage your media. They see who has no consent on file, and why a photo is hidden.
- Use MediaGrid for a grid of tiles that opens it, with focus back on the photo you were looking at.

## Avoid it for

- One photo that never needs to be seen larger. Use MediaImage.
- A gallery of documents or files. It is for photos and videos of people.
- Actions that change many photos. Its actions are for the photo shown.

## Props

- `items`: `MediaItem[]`. Required. The photos and videos, in order. See MediaImage for MediaItem.
- `index`: `number`. Default `0`. The photo it opens on. While it is open, it follows a new index.
- `open`: `boolean`. Controlled: shown while true. Pair it with onOpenChange.
- `defaultOpen`: `boolean`. Default `false`. Opens on mount, like a link to one photo.
- `onOpenChange`: `(open: boolean, index: number) => void`. It opened or closed, with the photo it was on. It closes on Esc, Close, and a press on the space around the photo.
- `onClose`: `(index: number) => void`. It closed by Esc or Close, with the photo it was on.
- `onIndexChange`: `(index: number) => void`. It moved to another photo.
- `manage`: `boolean`. Default `false`. For people who manage your media: names in consent notes, and why a photo is hidden.
- `actions`: `(item: MediaItem) => { icon: ReactNode; label: string; onClick: () => void }[]`. Icon buttons beside Close for the photo shown, like Download, Share, and Delete.
- `label`: `string`. Default `"Photos"`. The set's name. It titles the viewer and is read as it opens.
- `photoConsent`: `{ who?: "everyone" | "juniors" | "nobody"; missing?: "blur" | "hide" }`. Default `{ who: "everyone", missing: "blur" }`. Your consent rule. See MediaImage.
- `returnFocus`: `(index: number) => HTMLElement | null | undefined`. Where focus goes as it closes, given the photo it was on. By default it goes back where it was.
- `inline`: `boolean`. Default `false`. Draws it in the page's flow, with no backdrop and no Close, like a picture of the viewer in a doc.
- `className`: `string`. Extra classes on the viewer. Later classes win.

## Accessibility

- A modal dialog named by label. The page behind is inert and does not scroll, and Tab stays inside, even when pressed fast.
- As it opens, focus moves onto the viewer itself, so its name is read. A page that does not have focus keeps it where it is.
- ← and → move between photos without wrapping, and Home and End go to the ends. Right to left, the arrows swap. Each move is said in a polite live region, like '2 of 5. Launch'.
- Previous and Next stay focusable at the ends, so focus is never lost when one stops moving.
- Esc closes it, even from Close's tooltip, which teaches the key. Focus goes back where it was, or to returnFocus.
- The caption names only people with nothing held back. Consent notes are words, not only a blur.

## Examples

### From a photo's button

```tsx
const [open, setOpen] = useState(false);

<button type="button" onClick={() => setOpen(true)} aria-label="View photos from the launch">
  <MediaImage item={photos[0]} alt="" className="size-32" />
</button>
<Lightbox items={photos} open={open} onOpenChange={setOpen} label="Photos from the launch" />
```

### With actions, for a media manager

```tsx
import { Download, Trash2 } from "lucide-react";

<Lightbox
  items={photos}
  open={open}
  onOpenChange={setOpen}
  manage
  actions={(photo) => [
    { icon: <Download />, label: "Download", onClick: () => download(photo) },
    { icon: <Trash2 />, label: "Delete", onClick: () => remove(photo) },
  ]}
/>
```
