Skip to content
goodtabs UI
BrowseBrowse components

Gallery section

A captioned image grid where every image shares one aspect ratio.

npx shadcn@latest add https://ui.goodtabs.co/r/gallery-section.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/gallery-section

Grey spheres on a pale ground
SpheresStudy 01
Diagonal grey stripes
StripesStudy 02

Two captioned images in two columns.

Three columns

A row of grey arches
ArchesRender, 2026
Stacked grey blocks
BlocksRender, 2026
Soft grey dunes
DunesRender, 2026
Fine parallel lines
LinesRender, 2026
Concentric grey rings
RingsRender, 2026
Overlapping browser tabs
TabsRender, 2026

Six images, two columns narrow and three from 42rem.

Usage

import { GallerySection } from "@/components/gallery-section"

<GallerySection items={[{ src: "/work/one.jpg", alt: "A dashboard in dark mode", caption: "Dashboard", meta: "2026" }]} />

Props

PropTypeDefaultDescription
items{ src; alt; caption; meta? }[]The images. alt is required because the image carries meaning.
columns2 | 33Maximum columns. Two columns on narrow containers.

Source

gallery-section/gallery-section.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

type GalleryItem = {
  src: string;
  alt: string;
  caption: string;
  /** A mono line under the caption: a year, a place, a credit. */
  meta?: string;
};

type GallerySectionProps = Omit<React.ComponentProps<"div">, "children"> & {
  items: GalleryItem[];
  /** Columns once the container is wide enough. */
  columns?: 2 | 3;
};

const COLUMNS = { 2: "grid-cols-2", 3: "grid-cols-2 @2xl:grid-cols-3" } as const;

/**
 * A captioned image grid. Every image keeps one aspect ratio, so captions line up in rows.
 */
export function GallerySection({ items, columns = 3, className, ...props }: GallerySectionProps) {
  return (
    <div data-slot="gallery-section" className={cn("@container w-full", className)} {...props}>
      <div className={cn("grid gap-x-4 gap-y-6", COLUMNS[columns])}>
        {items.map((item) => (
          <figure key={item.src} className="m-0 flex flex-col gap-2">
            <div className="overflow-hidden rounded-lg border bg-muted">
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img src={item.src} alt={item.alt} loading="lazy" className="aspect-3/2 w-full object-cover" />
            </div>
            <figcaption className="flex flex-col gap-0.5">
              <span className="text-sm leading-5 font-medium text-foreground">{item.caption}</span>
              {item.meta && <span className="font-mono text-xs leading-4 text-muted-foreground">{item.meta}</span>}
            </figcaption>
          </figure>
        ))}
      </div>
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/gallery-section/prompt.md, or the Copy AI prompt button above.

When to use

  • Showing work, screenshots or a set of renders
  • A portfolio or case study page
  • Captioned images that should line up in rows

When not to use

  • Full screen browsing, which wants a lightbox or carousel
  • Post teasers with text, which want blog cards
  • A single hero image

Agent notes

  • Server component; uses figure and figcaption so captions are tied to images
  • Layout follows container width with @container
  • Every image is cropped to aspect-3/2, crop in the source if you need a different focal point