Contact Sheet Gallery
A grid gallery laid out like a photographic contact sheet, complete with sprocket-hole film edges and frame numbers; hover circles a frame in grease pencil, and the selected frame enlarges into a work print with a handwritten annotation.
npx shadcn@latest add https://webberui.com/r/contact-sheet-gallery.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ContactSheetGallery />
Installation
npx shadcn@latest add https://webberui.com/r/contact-sheet-gallery.jsonOr, once registries are configured in components.json, install it as @webberui/contact-sheet-gallery.
Usage
import {
ContactSheetGallery,
ContactSheetFrame,
} from "@/components/ui/contact-sheet-gallery";
<ContactSheetGallery columns={4} filmLabel="WEBBER 400TX">
<ContactSheetFrame id="a" annotation="This one ✓">
<img src="/photos/a.jpg" alt="Street scene" />
</ContactSheetFrame>
<ContactSheetFrame id="b">
<img src="/photos/b.jpg" alt="Portrait" />
</ContactSheetFrame>
</ContactSheetGallery>;A frame can hold an <img>, a gradient div, or any node; an <img> is automatically object-cover'd to fill the frame. Clicking a frame enlarges it into a work print, and clicking the background, pressing the close button, or hitting Esc all collapse it.
Controlled selection
Take over the selected state with selectedId / onSelectChange to sync from outside or open and close the work print programmatically:
const [selected, setSelected] = React.useState<string | null>(null);
<ContactSheetGallery selectedId={selected} onSelectChange={setSelected}>
{/* ...ContactSheetFrame */}
</ContactSheetGallery>;Props
ContactSheetGallery
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Put ContactSheetFrame children here |
columns | number | 4 | Frames per row |
selectedId | string | null | — | Controlled id of the selected frame (null for none) |
defaultSelectedId | string | null | null | Initially selected frame in uncontrolled mode |
onSelectChange | (id: string | null) => void | — | Fires when the selected frame changes (including null on close) |
filmLabel | string | "WEBBER 400" | Film-edge lettering printed along the bottom |
markColor | string | "#e2482f" | Color of the grease pencil marks |
stagger | number | 0.05 | Interval between frames on reveal (seconds) |
ContactSheetFrame
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique key |
frameNumber | string | number | automatic | Frame number; numbered by position when unspecified, and values like "24A" are accepted |
annotation | ReactNode | — | Handwritten-style annotation shown when enlarged |
label | string | 第 N 格 | Frame description for assistive technology; falls back to 第 N 格 ("frame N") when not supplied — the built-in default is Traditional Chinese, so pass this prop to word it in your own language |
children | ReactNode | — | Image content inside the frame |
How it works
- Film layout: the sprocket-hole edge strips are tiled with an SVG
pattern, so their width adapts and is independent of the frame count; the bottom edge is printed with the film-stock marking and a direction arrow. - Grease pencil circling: on hover or keyboard focus, a crayon ellipse is drawn in a single stroke through SVG
pathLength; the mark usesnon-scaling-stroke, so it hugs a frame of any proportion while keeping a constant line width. - Work print: selecting a frame enlarges it into a work print through a shared
layoutId, with the image "pulled out" of its original frame and an empty frame left behind; the annotation fades in in a handwritten face as its underline is drawn. - The colors stay on a neutral scale throughout, with
markColoras the only accent, adjustable per project.
Accessibility
- Every frame is a native
<button>supportingEnter/Spaceto select, carryingaria-pressedand anaria-label(falling back to第 N 格, "frame N", whenlabelis not supplied). - The work print is
role="dialog"+aria-modal, focusing the close button automatically when it opens and returning focus to the original frame once closed;Esc, clicking the background, and the close button all collapse it. - When the user has "reduce motion" enabled at the system level, the reveal, the circling stroke, and the enlargement offset are disabled and it switches straight to the static layout.
Agenda Tracks Grid
A time × track conference agenda grid — the timeline column freezes to the left, session cards reflow into place with FLIP when tracks or speakers are filtered, and clicking a session expands the speaker and summary in place.
Leader Line Captions
Magazine-style leader-line callouts — numbered captions arranged along a sidebar, connected by SVG leader lines to detail points in the image, drawn one by one on reveal and dimming into focus on hover.