All pages

Components

Image Viewer

A full-screen look at an attached image, opened from the composer's thumb row and from images on a sent message. Click a thumbnail below to open it; arrow keys page, Escape or the scrim closes.

Usage

Mount-to-open rather than an open prop: which image the viewer starts on is whichever thumb was pressed, and remounting is what resets that. The exit fade comes from useDialogDismiss — the whole surface, chrome and photograph together, on the same scrim pair every other dialog uses.

import { ImageViewer } from "@/components/base/image-viewer";

{start !== null ? (
<ImageViewer images={images} startIndex={start} onClose={() => setStart(null)} />
) : null}

Fixed colours

Every colour here is fixed rather than tokenised, and deliberately so: the ground is a near-black scrim over the entire page, and the discs sit on the photograph rather than on the page — white on dark in both themes, sized for a thumb on touch.

Behaviour

The scrim is the dismiss target — anywhere off the image closes, which is what a viewer opened by a tap should answer to. The body is scroll-locked while it is up (with overscroll-contain as belt to iOS Safari's braces), focus returns to the thumb that opened it, and the index is read through a clamp because an upload can land while the viewer is open. Stepping between pictures re-runs only the image's own fade, keyed on the URL — the step is not the open, and the surface must not replay.