# Image Result Gallery

Image Result Gallery [#image-result-gallery]

The **Image Result Gallery** is a responsive grid/list of image result cards. Each card shows the image in an aspect container, an in-flight processing overlay, a **persistent metadata caption** (filename + category badge + a confidence score badge) that stays visible at rest on every viewport — scannable without a hover, so it is keyboard-, touch- and AT-reachable — a multi-select checkbox, and a delete affordance, with a staggered fade-in. Grid and list layouts share **one data contract** (`ImageResultCard`), so apps switch layout without reshaping data.

Per-card scores render through a self-contained fallback score badge so this component installs and builds on its own. Its color tiers are configurable via the `scoreThresholds` prop — pass CLIP-scaled breakpoints (e.g. `{ high: 0.35, medium: 0.2 }`) so cross-modal similarity scores (which land in \~0.15–0.35) aren't misread as red "low" failures. It also declares the richer [`ConfidenceScoreBadge`](/docs/results/confidence-score-badge) (Results family) as a registry dependency — when that is installed you can swap the internal badge for it.

**When to use it:** smart galleries, duplicate finders, product search, batch image classification/captioning — anywhere you display a set of image results with scores and selection.

Preview [#preview]

```tsx
'use client';

import * as React from 'react';
import { ImageResultGallery } from '@/components/image-result-gallery';
import type { ImageResultCard } from '@/components/image-result-gallery';

/** Inline solid-color SVG thumbnails — no network request in the preview. */
function tile(color: string, text: string) {
  return `data:image/svg+xml;utf8,${encodeURIComponent(
    `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="${color}"/><text x="100" y="108" font-family="sans-serif" font-size="18" fill="#fff" text-anchor="middle">${text}</text></svg>`,
  )}`;
}

const INITIAL: ImageResultCard[] = [
  { id: '1', src: tile('#0ea5e9', 'cat'), label: 'tabby cat', category: 'animal', score: 0.97 },
  { id: '2', src: tile('#22c55e', 'tree'), label: 'oak tree', category: 'nature', score: 0.88 },
  { id: '3', src: tile('#f59e0b', 'car'), label: 'sports car', category: 'vehicle', score: 0.62 },
  { id: '4', src: tile('#ec4899', 'bird'), label: 'robin', category: 'animal', score: 0.79 },
  { id: '5', src: tile('#14b8a6', 'cup'), label: 'coffee cup', category: 'object', score: 0.91 },
  { id: '6', src: tile('#f43f5e', 'rose'), label: 'red rose', category: 'nature', score: 0.84 },
  { id: '7', src: tile('#6366f1', 'book'), label: 'paperback', category: 'object', score: 0.55 },
  { id: '8', src: tile('#8b5cf6', '…'), label: 'analyzing…', category: 'pending', processing: true },
];

// Cross-modal CLIP similarity scores compress into ~0.15–0.35 — with the
// default 0.8/0.5 tiers every strong match would render as a red "low" badge, so
// this row passes CLIP-tuned `scoreThresholds` to color them correctly.
const CLIP_RESULTS: ImageResultCard[] = [
  { id: 'c1', src: tile('#0ea5e9', 'A'), label: 'best match', category: 'search', score: 0.34 },
  { id: 'c2', src: tile('#22c55e', 'B'), label: 'good match', category: 'search', score: 0.27 },
  { id: 'c3', src: tile('#f59e0b', 'C'), label: 'weak match', category: 'search', score: 0.16 },
];

/**
 * Demo for the ImageResultGallery, used by the docs live preview. Static cards
 * (no model download) exercise grid/list layout switching, multi-select,
 * delete, the per-card processing overlay, the persistent (non-hover) metadata
 * caption, and the CLIP-tuned `scoreThresholds` prop.
 */
export default function ImageResultGalleryDemo() {
  const [cards, setCards] = React.useState(INITIAL);
  const [selected, setSelected] = React.useState<string[]>([]);
  const [layout, setLayout] = React.useState<'grid' | 'list'>('grid');

  return (
    <div className="w-full max-w-xl space-y-3">
      <div className="inline-flex rounded-lg border border-border bg-muted p-1 text-sm">
        <button
          type="button"
          onClick={() => setLayout('grid')}
          className={layout === 'grid' ? 'rounded-md bg-background px-3 py-1 font-medium shadow-sm' : 'px-3 py-1 text-muted-foreground'}
        >
          Grid
        </button>
        <button
          type="button"
          onClick={() => setLayout('list')}
          className={layout === 'list' ? 'rounded-md bg-background px-3 py-1 font-medium shadow-sm' : 'px-3 py-1 text-muted-foreground'}
        >
          List
        </button>
      </div>

      <ImageResultGallery
        cards={cards}
        layout={layout}
        selectedIds={selected}
        onSelect={(id, on) =>
          setSelected((prev) => (on ? [...prev, id] : prev.filter((x) => x !== id)))
        }
        onDelete={(id) => {
          setCards((prev) => prev.filter((c) => c.id !== id));
          setSelected((prev) => prev.filter((x) => x !== id));
        }}
      />

      {selected.length > 0 && (
        <p className="text-sm text-muted-foreground">{selected.length} selected</p>
      )}

      <div className="space-y-2 border-t border-border pt-3">
        <p className="text-sm font-medium">
          CLIP-scale scores{' '}
          <code className="rounded bg-muted px-1 py-0.5 text-xs">
            scoreThresholds={'{{ high: 0.35, medium: 0.2 }}'}
          </code>
        </p>
        <p className="text-xs text-muted-foreground">
          Cross-modal similarity lands ~0.15-0.35; tuned thresholds keep strong
          matches from rendering as a red &ldquo;low&rdquo; badge.
        </p>
        <ImageResultGallery
          cards={CLIP_RESULTS}
          layout="grid"
          scoreThresholds={{ high: 0.35, medium: 0.2 }}
        />
      </div>
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/media-vision/image-result-gallery
```

Dependencies [#dependencies]

* **Data source:** renders the `ImageResultCard[]` you pass — works with any backend that yields image results. Recommended LocalMode producers: `useClassifyImageZeroShot` (label + score), `useEmbedImage` (similarity results), or `useCaptionImage` (caption as the label) from `@localmode/react` (optional).

* `lucide-react` — icons

* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)

Registry dependencies [#registry-dependencies]

* `@localmode/ui/results/confidence-score-badge` — pulled in on install for the richer score badge (a minimal fallback is inlined so the gallery works standalone)

Files installed [#files-installed]

* `image-result-gallery.tsx` — the component (fade-in keyframes shipped inline)
* `confidence-score-badge.tsx` — the richer score badge (pulled in as a registry dependency)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**ImageResultGallery**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `cards` | `array` | — | **Required.** The cards to render. |
| `layout` | `"list" \| "grid"` | `"grid"` | Layout. |
| `selectedIds` | `array` | — | Currently-selected card ids (controlled multi-select). |
| `scoreThresholds` | `object` | `{ high: 0.8, medium: 0.5 }` | Score-tier breakpoints for the per-card badge color. The default suits softmax probabilities (0–1); override it for compressed score ranges such as cross-modal CLIP similarity (~0.15–0.35). |
| `onSelect` | `function` | — | Called with a card id when its selection checkbox is toggled. |
| `onDelete` | `function` | — | Called with a card id when its delete affordance is clicked. |

**ImageResultCard**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | **Required.** Stable identifier (used as the React key + selection/delete handle). |
| `src` | `string` | — | **Required.** Image source (data URL or URL). |
| `label` | `string` | — | Primary label/caption (e.g. top class, generated caption). |
| `category` | `string` | — | Secondary category/tag shown in the card metadata. |
| `score` | `number` | — | Confidence score in `[0, 1]`. When set, a score badge is shown. |
| `processing` | `boolean` | — | When true, render the per-card in-flight processing overlay. |

**ScoreThresholds**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `high` | `number` | `0.8` | Scores at or above this render as the high (emerald) tier. |
| `medium` | `number` | `0.5` | Scores at or above this (but below `high`) render as the medium (amber) tier. |

Backing hooks [#backing-hooks]

Build the cards from `useClassifyImageZeroShot` (label + score), `useEmbedImage` (similarity results), or `useCaptionImage` (caption as the label) from [`@localmode/react`](https://localmode.dev/docs/react).

Examples [#examples]

Grid with selection and delete [#grid-with-selection-and-delete]

```tsx
import { ImageResultGallery, type ImageResultCard } from '@/components/image-result-gallery';

export function Example({ cards }: { cards: ImageResultCard[] }) {
  const [selected, setSelected] = useState<string[]>([]);

  return (
    <ImageResultGallery
      cards={cards}
      layout="grid"
      selectedIds={selected}
      onSelect={(id, on) =>
        setSelected((prev) => (on ? [...prev, id] : prev.filter((x) => x !== id)))
      }
      onDelete={(id) => remove(id)}
    />
  );
}
```

List layout, same data [#list-layout-same-data]

```tsx
<ImageResultGallery cards={cards} layout="list" />
```

CLIP-scale similarity scores [#clip-scale-similarity-scores]

Cross-modal (text→image / image→image) CLIP similarity scores compress into roughly `0.15–0.35`. Pass tuned `scoreThresholds` so the strongest matches read as high/medium instead of a red "low" badge:

```tsx
<ImageResultGallery
  cards={searchResults}
  layout="grid"
  scoreThresholds={{ high: 0.35, medium: 0.2 }}
/>
```

From classification results [#from-classification-results]

```tsx
const { data } = useClassifyImageZeroShot({ model });

const cards: ImageResultCard[] = images.map((img) => ({
  id: img.id,
  src: img.src,
  label: img.top?.label,
  score: img.top?.score,
  processing: img.pending,
}));
```

Customization [#customization]

The staggered fade-in keyframes are shipped inline, so the gallery animates standalone after `shadcn add`. Adjust the grid column counts, the persistent caption gradient, or the score tiers (per-instance via `scoreThresholds`, or globally by editing the `ScoreBadge` tones) in the copied `image-result-gallery.tsx`. To use the full Results-family badge, install `@localmode/ui/results/confidence-score-badge` and replace the internal `ScoreBadge`.

Accessibility [#accessibility]

The metadata caption (filename + category + score) is **persistent** — it stays visible at rest on every viewport rather than appearing only on hover, so it is reachable by keyboard, touch, and assistive tech without a pointer. Each card `<img>` carries intrinsic `width` / `height` plus `loading="lazy"` and `decoding="async"`, so the grid reserves its aspect box up front (no cumulative layout shift as thumbnails decode). Score badges are colored by the `scoreThresholds` bands, so a low CLIP-scale similarity isn't misread as a red failure — pass CLIP-tuned breakpoints for cross-modal galleries.