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


Two captioned images in two columns.
Three columns






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
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { src; alt; caption; meta? }[] | The images. alt is required because the image carries meaning. | |
| columns | 2 | 3 | 3 | Maximum 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