# Grayscale Mosaic With Lightbox

> Photo mosaic in a fixed four-by-two grid whose tiles shift from grayscale to color on hover and open a lightbox with caption and location.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/gallery-4).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/gallery-4
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/gallery-4-light-a5ed67f5ca41403f.png
- Registry item: https://7ovr.com/r/gallery-4.json
- Categories: gallery
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/gallery-4.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `badge`, `dialog`

## Files

### `registry/blocks/gallery/4/gallery-block.tsx`

```tsx
"use client"

import * as React from "react"
import { Badge } from "@/components/ui/badge"
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogTitle,
} from "@/components/ui/dialog"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type Photo = {
  id: number
  src: string
  title: string
  location: string
}

const photos: Photo[] = [
  {
    id: 1,
    src: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=1200&q=80",
    title: "Financial District",
    location: "New York, USA",
  },
  {
    id: 2,
    src: "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=700&q=80",
    title: "Alpine Fog",
    location: "Grindelwald, CH",
  },
  {
    id: 3,
    src: "https://images.unsplash.com/photo-1500534623283-312aade485b7?w=700&q=80",
    title: "Coastal Dunes",
    location: "Jutland, DK",
  },
  {
    id: 4,
    src: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=700&q=80",
    title: "Pine Canopy",
    location: "Bavaria, DE",
  },
  {
    id: 5,
    src: "https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?w=700&q=80",
    title: "Still Water",
    location: "Hallstatt, AT",
  },
]

export default function GalleryBlock() {
  const [active, setActive] = React.useState<Photo | null>(null)

  return (
    <section className="flex min-h-svh w-full justify-center bg-background px-6 py-16 text-foreground">
      <div className="w-full max-w-4xl">
        <div className="mb-8 flex items-end justify-between gap-4">
          <div>
            <Badge variant="secondary">Field Notes</Badge>
            <h2 className="mt-3 font-heading text-2xl font-bold tracking-tight text-balance sm:text-3xl">
              Selected Work
            </h2>
          </div>
          <span className="text-sm text-muted-foreground tabular-nums">
            {photos.length} Photos
          </span>
        </div>

        <Dialog
          open={active !== null}
          onOpenChange={(open) => !open && setActive(null)}
        >
          <div className="grid aspect-[3/2] grid-cols-4 grid-rows-2 gap-2">
            {photos.map((photo, index) => (
              <button
                key={photo.id}
                type="button"
                onClick={() => setActive(photo)}
                className={
                  "group relative overflow-hidden rounded-lg border border-border bg-muted " +
                  (index === 0 ? "col-span-2 row-span-2" : "")
                }
              >
                <img
                  src={photo.src}
                  alt={photo.title}
                  className="size-full object-cover grayscale transition-all duration-300 group-hover:grayscale-0"
                  loading="lazy"
                />
                <span className="absolute inset-0 flex items-center justify-center bg-foreground/0 opacity-0 transition-opacity group-hover:bg-foreground/10 group-hover:opacity-100">
                  <span className="flex size-9 items-center justify-center rounded-md bg-background/90 text-foreground">
                    <IconPlaceholder
                      lucide="ZoomIn"
                      tabler="IconZoomIn"
                      hugeicons="SearchAddIcon"
                      phosphor="MagnifyingGlassPlus"
                      remixicon="RiZoomInLine"
                      className="size-4"
                      aria-hidden="true"
                    />
                  </span>
                </span>
              </button>
            ))}
          </div>

          <DialogContent className="overflow-hidden p-0 sm:max-w-2xl">
            {active && (
              <>
                <div className="aspect-video w-full overflow-hidden bg-muted">
                  <img
                    src={active.src}
                    alt={active.title}
                    className="size-full object-cover"
                  />
                </div>
                <div className="p-4 pt-0">
                  <DialogTitle className="text-base">
                    {active.title}
                  </DialogTitle>
                  <DialogDescription>{active.location}</DialogDescription>
                </div>
              </>
            )}
          </DialogContent>
        </Dialog>
      </div>
    </section>
  )
}
```
