Gallery Grid
Photos of the work or the place in an even grid or with the first image large, with captions.
npx shadcn@latest add @vibebox/gallery-gridControls
<GalleryGrid />About
Rebuilds the site kit's gallery. Free default for gallery and showcase pages.
Use it when
- The work or the place is visual (food, interiors, events, studios).
Skip it when
- Photos have very different shapes: use
gallery-masonry.
Install
- 1Add the VibeBox registry to
components.json:{ "registries": { "@vibebox": "https://motion.vibeboxph.com/r/{name}.json" } } - 2Add the block with the shadcn CLI:
npx shadcn@latest add @vibebox/gallery-grid - 3Use it:
import { GalleryGrid } from "@/components/vibebox/gallery-grid"; import { home } from "@/content/pages/home"; <GalleryGrid content={home.gallery} variant="uniform" id="gallery" />
Code
2 filescomponents/vibebox/gallery-grid/index.tsx
/**
* VibeBox Motion · Gallery Grid
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { Container, headingId, Media, REVEAL, Section, SectionHead, SlotText, type BlockProps } from "@/lib/vibebox/block-kit";
import { cn } from "@/lib/utils";
import type { Content, Variant } from "./schema";
/*
* Photos of the work or the place: an even grid of squares, or the first image as a wide banner
* over the rest. Captions sit under each image and can wait as fill-ins.
*/
export type GalleryGridProps = BlockProps<Content, Variant>;
export function GalleryGrid({ content, variant = "uniform", id, tone, className }: GalleryGridProps) {
const feature = variant === "feature";
return (
<Section slug="gallery-grid" variant={variant} id={id} tone={tone} labelledBy={headingId(id)} className={className}>
<Container className="grid gap-[clamp(32px,4vw,56px)]">
<SectionHead eyebrow={content.eyebrow} heading={content.heading} lead={content.lead} id={id} />
<ul className="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-3">
{content.items.map((item, index) => (
<li key={index} className={cn("grid content-start gap-2", feature && index === 0 && "col-span-2 lg:col-span-3", REVEAL)}>
<figure className="m-0 grid gap-2">
<div className={cn("overflow-hidden rounded-xl ring-1 ring-border", feature && index === 0 ? "aspect-[16/9] lg:aspect-[21/8]" : "aspect-[4/3] sm:aspect-square")}>
<Media image={item.media} variant={index} sizes={feature && index === 0 ? "100vw" : "(min-width: 1024px) 33vw, 50vw"} />
</div>
<SlotText as="figcaption" value={item.caption} path={`items.${index}.caption`} className="text-small text-muted-foreground" />
</figure>
</li>
))}
</ul>
</Container>
</Section>
);
}components/vibebox/gallery-grid/schema.ts
/**
* VibeBox Motion · Gallery Grid
* Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
* https://motion.vibeboxph.com/license
* Use it in unlimited personal, commercial and client projects.
* Do not sell or redistribute it as a component, kit, template or builder.
*/
import { sections, type z } from "@/lib/vibebox/content";
export const variants = ["uniform", "feature"] as const;
export type Variant = (typeof variants)[number];
export const content = sections.gallery;
export type Content = z.infer<typeof content>;Content
Typed slots from schema.ts. Facts (hours, prices, quotes, names) come only from the business; until then they render as fill-ins.
| Slot | Label | Type | Limit | Fact |
|---|---|---|---|---|
| eyebrow | Kicker | text | 32 chars | — |
| heading | Heading | text | 60 chars | — |
| lead | Intro | text | 160 chars | — |
| items | Images | list | up to 12 | — |
| items[].media | Image | image | — | — |
| items[].caption | Caption | text | 60 chars | Can be a fill-in |
Variants
Uniformdefault
uniform
Two columns on phones, three on desktop, square images.
Feature
feature
The first image spans two columns and rows; four columns on desktop.
Accessibility and motion
- Keyboard
- Nothing to focus.
- Screen readers
- A heading and a list of figures with alt text and captions.
- Touch
- Nothing to touch.
- Reduced motion
- Images appear without their reveal.
Edge cases and limits
- Twelve images make four rows.
- Missing photos: scenes, each in a different arrangement.
Related

Gallery Masonry
FreeImages at their own proportions in balanced columns, so portraits and landscapes sit together.
Gallery block · Server

A swipeable row that snaps one photo at a time, each turning in 3D as it leaves the centre.
Gallery block · Server

Before and after, compared by dragging a liquid seam across each photo, then the rest of the gallery.
Gallery block · WebGL2