# Gallery

> Images: Gallery (photos with thumbnails and full screen), BeforeAfter (drag to compare, e.g.

Component · Media · Pro kit · [Live page](https://ui.twentyfirst.media/pro/components/Gallery/)

Images: `Gallery` (photos with thumbnails and full screen), `BeforeAfter` (drag to compare, e.g. virtual staging or upscaling) and `ImageVariants` (the sizes and formats generated from one image).

Gallery: `items` ({src, alt, caption, art, tint}), `start`, `ratio`, `thumbs`, `fullscreen`, `badge`, `loading`, `referrerPolicy`. Arrow keys and Esc work in full screen.
BeforeAfter: `before`, `after` ({src, alt} or {art}), `start` (0–100), `ratio`, `beforeLabel`, `afterLabel`, `label`, `valueText`, `loading`, `referrerPolicy`. Drag, click or use ← → (Page Up/Down for bigger steps, Home and End for the ends).
ImageVariants: `variants` ({id, label, src, size, format, weight, ratio, status}), `selected` / `onSelect`, `alt`, `tint`, `actions`.

- Images load lazily (`loading="eager"` for a gallery at the top of the page; thumbnails always load lazily) and decode off the main thread.
- `referrerPolicy="no-referrer"` keeps your page address from hosts the photos come from.
- The compare slider tells screen readers both shares ("Before 40%, After 60%", with your labels); `valueText` words it your way.

## Example

```tsx
import * as React from "react";
import { BeforeAfter, Gallery, ImageVariants } from "@twentyfirstmedia/pro";
import type { Tint } from "@twentyfirstmedia/pro";

const art = (t: Tint, s: string) => (
  <div
    style={{
      position: "absolute",
      inset: 0,
      background: "linear-gradient(160deg, var(--" + t + ") 0%, var(--surface) 120%)",
      display: "grid",
      placeItems: "center",
      color: "var(--" + t + "-ink)",
      font: "var(--type-title-weight) 14px/1 var(--font-sans)",
    }}
  >
    {s}
  </div>
);
const before = {
  art: (
    <div
      style={{
        position: "absolute",
        inset: 0,
        background: "linear-gradient(180deg, #ddd8cf 0 62%, #b9ad9c 62%)",
      }}
    />
  ),
};
const after = {
  art: (
    <div
      style={{
        position: "absolute",
        inset: 0,
        background: "linear-gradient(180deg, #e9e2d6 0 62%, #a68b6a 62%)",
      }}
    >
      <div
        style={{
          position: "absolute",
          left: "18%",
          bottom: "22%",
          width: "38%",
          height: "22%",
          borderRadius: "var(--radius-md)",
          background: "#5b6b5a",
        }}
      />
      <div
        style={{
          position: "absolute",
          right: "16%",
          bottom: "38%",
          width: "14%",
          height: "30%",
          borderRadius: "var(--radius-sm)",
          background: "#3d3a36",
        }}
      />
    </div>
  ),
};
export default function Preview() {
  return (
    <div
      className="tf-grid"
      style={
        {
          "--cols": "1fr 1fr",
          gap: 16,
          alignItems: "start",
        } as React.CSSProperties
      }
    >
      <div
        className="tf-panel"
        style={{
          gap: 12,
        }}
      >
        <Gallery
          badge="Virtual staging"
          items={[
            {
              art: art("sky", "Living room"),
              caption: "Living room",
            },
            {
              art: art("mint", "Kitchen"),
            },
            {
              art: art("butter", "Bedroom"),
            },
            {
              art: art("lilac", "Balcony"),
            },
            {
              art: art("blush", "Bathroom"),
            },
          ]}
        />
      </div>
      <div
        className="tf-col"
        style={{
          gap: 16,
        }}
      >
        <div
          className="tf-panel"
          style={{
            gap: 12,
          }}
        >
          <b>Virtual staging</b>
          <BeforeAfter before={before} after={after} afterLabel="Staged · Scandinavian" />
        </div>
        <div className="tf-panel">
          <ImageVariants
            tint="sky"
            variants={[
              {
                id: 1,
                label: "Original",
                size: "4032 × 3024",
                format: "JPEG",
                weight: "3.8 MB",
                ratio: "4 / 3",
              },
              {
                id: 2,
                label: "Listing",
                size: "1600 × 1200",
                format: "WebP",
                weight: "214 KB",
                ratio: "4 / 3",
                status: "ok",
              },
              {
                id: 3,
                label: "Square",
                size: "1080 × 1080",
                format: "WebP",
                weight: "168 KB",
                ratio: "1 / 1",
                status: "ok",
              },
              {
                id: 4,
                label: "Upscaled 2×",
                size: "8064 × 6048",
                format: "PNG",
                status: "running",
                ratio: "4 / 3",
              },
            ]}
          />
        </div>
      </div>
    </div>
  );
}
```

## Props

### BeforeAfter

| Prop | Type | Notes |
| --- | --- | --- |
| `before` | `MediaPane` | Required |
| `after` | `MediaPane` | Required |
| `start?` | `number` |  |
| `ratio?` | `string` |  |
| `beforeLabel?` | `string` |  |
| `afterLabel?` | `string` |  |
| `label?` | `string` | Accessible name of the slider (default "Compare before and after"). |
| `valueText?` | `(percent: number) => string` | What screen readers hear for a position (0–100, the share of "before"). Default: "Before 40%, After 60%", with your `beforeLabel`/`afterLabel`. |
| `loading?` | `"lazy" \| "eager"` | Loading of both images (default `"lazy"`). |
| `referrerPolicy?` | `ImageReferrerPolicy` | `referrerPolicy` of both images. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Gallery

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `GalleryItem[]` | Required |
| `loading?` | `"lazy" \| "eager"` | Loading of the main image: `"lazy"` (default) waits until the gallery is near the viewport; use `"eager"` for a gallery at the top of the page. Thumbnails always load lazily. |
| `referrerPolicy?` | `ImageReferrerPolicy` | `referrerPolicy` of every image, e.g. `"no-referrer"` for photos from other hosts. |
| `start?` | `number` | First image when uncontrolled. |
| `index?` | `number` | The shown image (controlled, 0-based), with `onIndexChange`. |
| `onIndexChange?` | `(index: number) => void` |  |
| `label?` | `string` | Accessible name of the gallery (default "Images"), e.g. the listing's title. |
| `ratio?` | `string` |  |
| `thumbs?` | `boolean` |  |
| `fullscreen?` | `boolean` |  |
| `badge?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ImageVariants

| Prop | Type | Notes |
| --- | --- | --- |
| `variants` | `ImageVariant[]` | Required |
| `selected?` | `string \| number` |  |
| `onSelect?` | `(id: string \| number) => void` |  |
| `alt?` | `string` |  |
| `tint?` | `Tint` |  |
| `actions?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
