---
title: Image Gallery
description: Responsive image grid with an accessible, keyboard-navigable lightbox
visibility: guest
draft: false
---

# Image Gallery

`<x-wirekit::image-gallery>` lays out user-content images in a responsive grid.
Clicking a thumbnail opens an accessible **lightbox** — a focus-trapped dialog
that navigates with the arrow keys, closes on Escape, and returns focus to the
thumbnail you opened it from. Each image is a [`<x-wirekit::image>`](/components/image)
figure, so alt text, lazy loading, and a CLS-safe ratio box come for free.

Pass an array of images, each with a `src` and an `alt` (and an optional
`caption`). A signed, ACL-protected download URL works unchanged.

## Usage

:::preview{title="Gallery with lightbox"}
<x-wirekit::image-gallery :images="[
    ['src' => '/placeholder/480x480?bg=0f172a&fg=ffffff&label=1&gradient=none', 'alt' => 'First photo', 'caption' => 'The first photo'],
    ['src' => '/placeholder/480x480?bg=4f46e5&fg=ffffff&label=2&gradient=none', 'alt' => 'Second photo'],
    ['src' => '/placeholder/480x480?bg=059669&fg=ffffff&label=3&gradient=none', 'alt' => 'Third photo'],
    ['src' => '/placeholder/480x480?bg=b91c1c&fg=ffffff&label=4&gradient=none', 'alt' => 'Fourth photo'],
]" />
:::

## Columns and ratio

Control the responsive grid with `columns` (forwarded to the grid) and the
thumbnail shape with `ratio`:

:::preview{title="Three columns, 4/3 thumbnails"}
<x-wirekit::image-gallery
    columns="1 sm:2 md:3"
    ratio="4/3"
    :images="[
        ['src' => '/placeholder/480x360?bg=1e293b&fg=ffffff&label=A&gradient=none', 'alt' => 'Photo A'],
        ['src' => '/placeholder/480x360?bg=7c3aed&fg=ffffff&label=B&gradient=none', 'alt' => 'Photo B'],
        ['src' => '/placeholder/480x360?bg=0891b2&fg=ffffff&label=C&gradient=none', 'alt' => 'Photo C'],
    ]"
/>
:::

## Static grid (no lightbox)

Set `:lightbox="false"` for a plain responsive grid of figures — no dialog, no
JavaScript:

:::preview{title="Static grid"}
<x-wirekit::image-gallery
    :lightbox="false"
    columns="2 md:4"
    :images="[
        ['src' => '/placeholder/300x300?bg=334155&fg=ffffff&label=1&gradient=none', 'alt' => 'One', 'caption' => 'One'],
        ['src' => '/placeholder/300x300?bg=475569&fg=ffffff&label=2&gradient=none', 'alt' => 'Two', 'caption' => 'Two'],
    ]"
/>
:::

## Per-Item Overlays

Pass an `itemOverlay` closure to layer a control over each thumbnail — a badge, a
per-image report button, or a required content label (a synthetic-media disclosure,
a license marker). The closure receives `($item, $i)` and returns the markup; it renders as a
**sibling** of the zoom trigger, not inside it, so the thumbnail still opens the
lightbox. The overlay wrapper is `pointer-events-none` — give any interactive control
inside it `pointer-events: auto` so it stays clickable without triggering the zoom.

Return a view or an `HtmlString` for HTML (a plain string is escaped):

:::preview{title="Per-item overlay badge"}
<x-wirekit::image-gallery
    columns="1 sm:2"
    :images="[
        ['src' => '/placeholder/480x480?bg=0f172a&fg=ffffff&label=1&gradient=none', 'alt' => 'Landscape photo'],
        ['src' => '/placeholder/480x480?bg=4f46e5&fg=ffffff&label=2&gradient=none', 'alt' => 'Portrait photo'],
    ]"
    :item-overlay="fn ($item, $i) => new \Illuminate\Support\HtmlString('<span style=\'position:absolute;top:0.5rem;left:0.5rem;padding:0.125rem 0.5rem;border-radius:var(--radius-wk-full);background:var(--color-wk-bg-inverse);color:var(--color-wk-text-inverse);font-size:var(--text-wk-xs);font-weight:600\'>New</span>')"
/>
:::

In real code the closure typically returns a partial view so the overlay can carry
its own components and logic:

```blade
<x-wirekit::image-gallery
    :images="$photos"
    :item-overlay="fn (array $item, int $i) => view('partials.photo-report', ['photo' => $item, 'index' => $i])"
/>
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `images` | `array` | `[]` | List of images — each an array with `src`, `alt`, and optional `caption`. A plain string is treated as a decorative `src`. |
| `columns` | `string` | `'2 md:3 lg:4'` | Responsive grid column spec (forwarded to the grid). |
| `gap` | `string` | `'md'` | Grid gap token. |
| `ratio` | `string\|null` | `'1/1'` | Thumbnail aspect-ratio (CLS-safe). |
| `fit` | `string` | `'cover'` | `object-fit` for each thumbnail. `'cover'` crops to a uniform grid (right for mixed-orientation sets); `'contain'` shows each image whole, letterboxed inside its ratio box — use it when a single portrait shot must not be cropped. |
| `lightbox` | `bool` | `true` | Enable the click-to-zoom lightbox. `false` renders a static grid. |
| `itemOverlay` | `Closure\|null` | `null` | Per-item overlay render-callback `fn($item, $i)`. Returns markup layered over thumbnail `$i` as a sibling of the zoom trigger — a badge, a report control, a synthetic-media label. Return a view or `HtmlString` for HTML. See [Per-Item Overlays](#per-item-overlays). |
| `scope` | `string\|null` | `null` | Scoped personalization key. |

## Accessibility

- Each thumbnail is a real `<button>` with an accessible name ("View image 2:
  …"), so the gallery is fully keyboard-operable.
- The lightbox is a `role="dialog"` `aria-modal="true"` overlay, **focus-trapped**
  with the same helper Modal and Drawer use — focus cannot escape to the page
  behind it, and it returns to the triggering thumbnail on close.
- Every image carries its `alt`; decorative images pass `alt=""`.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Enter` / `Space` | Open the lightbox on the focused thumbnail |
| `Escape` | Close the lightbox (focus returns to the thumbnail) |
| `←` | Previous image |
| `→` | Next image |
| `Tab` | Move between the lightbox controls (focus stays trapped inside) |
