# Gallery 5

> Large image with a column of thumbnails that swap it (from Marbrook)

A shadcn/ui block from the [@cnippet-blocks](https://blocks.cnippet.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/cnippet-blocks/gallery-5).

- Page, with a live preview: https://shadcn.exchange/r/cnippet-blocks/gallery-5
- Screenshot: https://shadcn.exchange/shot/cnippet-blocks/gallery-5/light.png
- Registry item: https://blocks.cnippet.dev/r/gallery-5.json
- Categories: gallery
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/cnippet-blocks/gallery-5.json
```

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

## Dependencies

- Registry items: `@cnippet/utils`

## Files

### `registry/default/sections/gallery-5/index.tsx`

```tsx
"use client";

import Image from "next/image";
import { useState } from "react";

import { cn } from "@/lib/utils";

import type { Surface } from "../_types/common";
import type { GalleryViewerContent } from "../_types/gallery";
import { defaultGallery5Content } from "./data";

const surfaceClass: Record<Surface, string> = {
  default: "bg-background",
  muted: "bg-muted",
  primary: "bg-primary",
  secondary: "bg-secondary",
};

/** One large image with a column of thumbnails that swap it. */
export default function Gallery5({
  className,
  content = defaultGallery5Content,
  surface = "default",
}: {
  /** Use `pb-0` when a detail section sits directly below in the same band. */
  className?: string;
  content?: GalleryViewerContent;
  surface?: Surface;
}) {
  const [active, setActive] = useState(0);
  const { images, title } = content;
  const current = images[active] ?? images[0];

  if (!current) return null;

  return (
    <section className={cn("py-20 sm:py-28", surfaceClass[surface], className)}>
      <div className="mx-auto grid max-w-7xl gap-4 px-5 sm:px-8 lg:grid-cols-12 lg:px-12">
        <div className="relative aspect-3/2 overflow-hidden rounded-lg lg:col-span-9">
          <Image
            alt={current.alt}
            className="object-cover"
            fill
            priority
            sizes="(min-width: 1024px) 75vw, 100vw"
            src={current.src}
          />
        </div>

        <div className="grid grid-cols-4 gap-4 lg:col-span-3 lg:grid-cols-1">
          {images.map((image, index) => (
            <button
              aria-current={index === active}
              aria-label={`Show view ${index + 1} of ${images.length} of ${title}`}
              className={cn(
                "relative aspect-4/3 cursor-pointer overflow-hidden rounded-md transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
                index === active
                  ? "opacity-100"
                  : "opacity-55 hover:opacity-100",
              )}
              key={image.src}
              onClick={() => setActive(index)}
              type="button"
            >
              <Image
                alt=""
                className="object-cover"
                fill
                sizes="(min-width: 1024px) 20vw, 25vw"
                src={image.src}
              />
              {index === active && (
                <span className="pointer-events-none absolute inset-0 rounded-md border-2 border-secondary" />
              )}
            </button>
          ))}
        </div>
      </div>
    </section>
  );
}
```

### `registry/default/sections/gallery-5/data.ts`

```ts
import type { GalleryViewerContent } from "../_types/gallery";

export const defaultGallery5Content: GalleryViewerContent = {
  images: [
    {
      alt: "Blackwood House — view 1 of 4",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786950553/templates/estate/marbrook/cndn_marbrook002.jpg",
    },
    {
      alt: "Blackwood House — view 2 of 4",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786954292/templates/estate/marbrook/cndn_marbrook016.jpg",
    },
    {
      alt: "Blackwood House — view 3 of 4",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786954292/templates/estate/marbrook/cndn_marbrook017.jpg",
    },
    {
      alt: "Blackwood House — view 4 of 4",
      src: "https://res.cloudinary.com/qgiongel/image/upload/q_auto/f_auto/v1786954292/templates/estate/marbrook/cndn_marbrook018.jpg",
    },
  ],
  title: "Blackwood House",
};
```

### `registry/default/sections/_types/common.ts`

```ts
/**
 * Shapes shared by every token-based section. Each section ships this file
 * alongside its own category types, so installing several sections simply
 * rewrites the same file.
 */

/** Which band a section sits on. A style choice, never part of `content`. */
export type Surface = "default" | "muted" | "primary" | "secondary";

export type ImageAsset = {
  alt: string;
  src: string;
};

export type LinkAction = {
  href: string;
  label: string;
};

/** Eyebrow + heading + optional lede, the header most sections open with. */
export type SectionIntro = {
  description?: string;
  eyebrow: string;
  title: string;
};

/** A figure that counts up when it scrolls into view. */
export type Stat = {
  decimals?: number;
  label: string;
  prefix?: string;
  suffix?: string;
  value: number;
};

/** A figure that is displayed as written ("1,240+", "4.9/5"). */
export type Highlight = {
  label: string;
  value: string;
};

export type FormField = {
  label: string;
  name: string;
  /** Only for `type: "select"`. The first option is the default. */
  options?: string[];
  placeholder?: string;
  required?: boolean;
  /** `full` spans both columns of a two-column form. */
  span?: "full" | "half";
  type?: "email" | "select" | "tel" | "text" | "textarea";
};

export type FormContent = {
  description?: string;
  fields: FormField[];
  note?: string;
  submitLabel: string;
  success: {
    description: string;
    resetLabel: string;
    title: string;
  };
  title: string;
};
```

### `registry/default/sections/_types/gallery.ts`

```ts
import type { ImageAsset, SectionIntro } from "./common";

/** One large image with a strip of thumbnails that swap it. */
export type GalleryViewerContent = {
  images: ImageAsset[];
  title: string;
};

/** Captioned images in a horizontal carousel. */
export type GalleryCarouselContent = SectionIntro & {
  items: {
    description: string;
    href?: string;
    image: ImageAsset;
    title: string;
  }[];
};
```
