# Image Gallery

> A React image gallery with masonry, cropped grid and justified rows layouts, tag filters, a lightbox and load more.

- Type: React component (Media)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add image-gallery`
- Docs: https://wingo-ui.com/components/image-gallery

## Interaction

Photos fade in over a shimmering placeholder that already has their shape, so nothing jumps while they load. Masonry keeps each photo's shape and fills the shortest column, so the order still reads left to right; grid crops every photo to one ratio; rows packs justified rows like a photos app, every row one height and edge to edge. Tap a filter chip and the photos that leave fade out while the rest glide into their new places on a spring (a plain fade under reduced motion); on a phone the chips sit in one row you can swipe sideways. Tap a photo, or press Enter on it, and it grows into the full-screen lightbox, then flies back into its tile on close. The photos are one Tab stop: Left / Right move to the previous / next photo, Up / Down to the one above or below, Home / End to the first and last. Captions show on hover and focus with a mouse and stay visible on touch screens. With hasMore a Load more button appears (it spins while the next page loads), or with loadMode inView the next page loads as you scroll near the end. Phones get two columns by default.

## Installation

```bash
npx wingo-ui@latest add image-gallery
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/image-gallery` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/image-gallery.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Toggle Group](https://wingo-ui.com/components/toggle-group) ([Markdown](https://wingo-ui.com/components/toggle-group.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ImageGallery, type GalleryImage } from "@/components/ui/image-gallery"

const photos: GalleryImage[] = [
  { src: "/photos/brooklyn-loft.jpg", alt: "Loft living room", width: 1600, height: 1000, caption: "Loft, Brooklyn", tags: ["Interiors"] },
  { src: "/photos/glacier-trail.jpg", alt: "Mountain trail", width: 1000, height: 1500, caption: "Glacier National Park", tags: ["Travel"] },
  { src: "/photos/road-bike.jpg", alt: "Steel road bike", width: 1200, height: 1200, caption: "Road bike", tags: ["Gear"] },
]

export function Portfolio() {
  return <ImageGallery images={photos} layout="masonry" filters showCaptions="hover" />
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `images` | `GalleryImage[]` |  | { src, alt, width?, height?, caption?, tags?, thumbnail?, srcSet?, sizes?, id? }. Pass width and height so each tile reserves its shape before the file loads; without them a photo uses aspectRatio until it loads. |
| `layout?` | `"masonry" \| "grid" \| "rows"` | `"masonry"` | masonry keeps every photo's shape in balanced columns, grid crops them to aspectRatio, rows fills justified rows of one height computed from the photo ratios. |
| `columns?` | `number \| { base?: number; sm?: number; md?: number; lg?: number; xl?: number }` | `"{ base: 2, md: 3, lg: 4 }"` | Masonry and grid columns, 1 to 6, per breakpoint (sm 640px, md 768px, lg 1024px, xl 1280px). A number is one count everywhere. Two on phones, three on tablets and four on desktops by default. |
| `gap?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Space between photos: 0, 4, 8 or 16px. |
| `aspectRatio?` | `string \| number` | `"1/1"` | The grid layout's crop, as "4/3", "4:3" or a number. Masonry and rows use it only for photos without width and height until they load. |
| `targetRowHeight?` | `number` | `240` | Rows layout: the height each row aims for in px. Rows stretch a little to fill the width; below 640px they aim for 60% of it. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Tile corners: 0, 8, 12 or 16px; full falls back to lg like every surface. |
| `showCaptions?` | `"hover" \| "always" \| "none"` | `"hover"` | Captions over the bottom of each photo: on hover and keyboard focus (always visible on touch screens), always, or never. The lightbox shows them either way. |
| `hoverZoom?` | `boolean` | `true` | The photo zooms in 4% inside its tile on hover. |
| `skeleton?` | `"shimmer" \| "pulse" \| "none"` | `"shimmer"` | The placeholder under each photo until it loads. |
| `filters?` | `boolean \| string[]` | `false` | Tag chips above the photos with an All chip first. true collects the tags from the images in order; an array sets which tags show and their order. |
| `filterVariant?` | `"outline" \| "soft" \| "ghost"` | `"outline"` | The look of the filter chips (the toggle group's variants). |
| `filter?` | `string` |  | The active tag, controlled; "" shows all. Tags that are not in the chips count as all. |
| `defaultFilter?` | `string` | `""` | The tag active on the first render, uncontrolled. The playground remounts the gallery when it changes. |
| `onFilterChange?` | `(filter: string) => void` |  | Called with the new tag, or "" for All. |
| `lightbox?` | `boolean` | `true` | A tap or Enter opens the full-screen ImageLightbox at that photo, zooming out of the tile. Off, tiles are buttons only when onImageClick is set. |
| `lightboxProps?` | `Omit<ImageLightboxProps, "images" \| "open" \| "onOpenChange" \| "index" \| "onIndexChange" \| "getOrigin">` |  | Passed to the lightbox: loop, zoom, download, share, showThumbnails, labels and the rest. |
| `onImageClick?` | `(image: GalleryImage, index: number) => void` |  | Called when a photo is tapped or opened with the keyboard, before the lightbox opens. index is its place among the visible photos. |
| `hasMore?` | `boolean` | `false` | More photos exist: shows the Load more button (and, with loadMode inView, loads them as the end scrolls into view). |
| `loadMode?` | `"button" \| "inView"` | `"button"` | button waits for a tap on Load more; inView calls loadMore when the end of the gallery comes within 400px of the screen, and keeps the button as a fallback. |
| `loadMore?` | `() => void \| Promise<unknown>` |  | Fetches the next page. Return a promise and the button spins until it settles; never called twice at once. |
| `loading?` | `boolean` | `false` | The next page is on its way (for a loadMore that does not return a promise): the button spins and in-view loading pauses. |
| `labels?` | `Partial<ImageGalleryLabels>` |  | Every string: gallery, filters, all, loadMore, loading, empty, emptyFiltered, error, results(count) for the live region and openImage(index, total, alt) for each tile. |
| `classNames?` | `Partial<Record<"filters" \| "layout" \| "item" \| "tile" \| "image" \| "skeleton" \| "caption" \| "empty" \| "footer" \| "loadMore", string>>` |  | Extra classes for the inner parts: the chip row, the layout container, each list item, the tile button, the photo, its placeholder, the caption, the empty state and the load more row and button. |
| `className?` | `string` |  | Extra classes for the root, merged last. |

## Parts

### Masonry (component)

The balanced masonry layout on its own, for any keyed children: cards, testimonials, notes. Each child goes to the shortest column in order and glides to its new place when the set changes; the server renders css columns, so the first paint already has the right columns. Takes div props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `columns?` | `number \| { base?: number; sm?: number; md?: number; lg?: number; xl?: number }` | `"{ base: 2, md: 3, lg: 4 }"` | Columns per breakpoint, 1 to 6. |
| `gap?` | `"none" \| "sm" \| "md" \| "lg" \| number` | `"md"` | A step of the gap scale (0 / 4 / 8 / 16px) or px. |
| `animate?` | `boolean` | `true` | Items glide on a spring when children come and go; off under reduced motion. |
| `classNames?` | `Partial<Record<"item", string>>` |  | Extra classes for the wrapper around each child. |

### GalleryImage (type)

One photo: { id?, src, alt, width?, height?, caption?, tags?, thumbnail?, srcSet?, sizes?, downloadName? }.

### ImageGalleryLabels (type)

The strings the gallery shows and announces.

### DEFAULT_IMAGE_GALLERY_LABELS (constant)

The English labels, to spread and override for another language.

## Examples

- **Masonry with filters**: Tag chips with an All chip; photos that stay glide into place while the rest fade out.
- **Justified rows**: Rows of one height that fill the width, computed from each photo's ratio.
- **Square grid**: Cropped to 1:1 with square corners: three columns on phones, six on desktops.
- **Load on scroll**: loadMode inView fetches the next page as the end scrolls near; the button stays as a fallback.
- **Masonry for any content**: The Masonry part on its own, balancing review cards of any height.
- **Pick a photo**: lightbox off with onImageClick: the tiles become a picker for a listing cover.

## See also

- [Image Lightbox](https://wingo-ui.com/components/image-lightbox) ([Markdown](https://wingo-ui.com/components/image-lightbox.md))
- [Product gallery](https://wingo-ui.com/components/product-gallery) ([Markdown](https://wingo-ui.com/components/product-gallery.md))
- [Carousel](https://wingo-ui.com/components/carousel) ([Markdown](https://wingo-ui.com/components/carousel.md))
- [Toggle Group](https://wingo-ui.com/components/toggle-group) ([Markdown](https://wingo-ui.com/components/toggle-group.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))

---

Canonical: https://wingo-ui.com/components/image-gallery
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
