Click a thumbnail and it morphs: the same image element grows from its grid slot into a fullscreen detail via a shared-layout transition. Move between photos with the arrows or keys; close and it morphs back into place.
pnpm dlx shadcn@latest add "https://godui.design/r/lab/morph-gallery.json"
The grid image and the detail image share a layoutId, so opening and closing
animate the same element between the two positions with a spring, so there is no
cross-fade seam. The detail view traps focus, restores it to the originating tile on close,
and supports arrow-key and button navigation.
import { MorphGallery } from "@/components/godui/morph-gallery";
<MorphGallery
columns={3}
onOpenChange={(index) => console.log(index)}
items={[
{ src: "/photo-1.jpg", alt: "Coastline", caption: "Coastline at dawn" },
{ src: "/photo-2.jpg", alt: "Ridge", caption: "Alpine switchbacks" },
]}
/>
columns sets the grid width (2 to 5); it drops to fewer columns on small screens.
<MorphGallery columns={4} items={items} />
Each tile is a labelled button. The detail view is a modal dialog:
← / → move between photos, Esc closes, focus is
trapped inside while open and restored to the tile on close. When
prefers-reduced-motion is set, the shared-element morph is replaced with a
cross-fade.
| Prop | Type | Default | Description |
|---|
items | MorphGalleryItem[] | none | Images: { src, alt, caption? }. |
columns | 2 | 3 | 4 | 5 | 3 | Grid column count. |
onOpenChange | (index: number | null) => void | none | Fired when the detail view opens or closes. |
MorphGallery also forwards every standard <div> attribute to the root element.