Skip to content
Motion
FreeIn reviewv1.0.0

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-grid

Controls

Variant
<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

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the block with the shadcn CLI:
    npx shadcn@latest add @vibebox/gallery-grid
  3. 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 files
components/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.

SlotLabelTypeLimitFact
eyebrowKickertext32 chars—
headingHeadingtext60 chars—
leadIntrotext160 chars—
itemsImageslistup to 12—
items[].mediaImageimage——
items[].captionCaptiontext60 charsCan 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.
  • Scenes of different proportions in three balanced columns with captions

    Images at their own proportions in balanced columns, so portraits and landscapes sit together.

    Gallery block · Server

  • A row of photos, the centre one facing forward and the others turned away

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

    Gallery block · Server

  • A living room photo split by a liquid seam between before and after

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

    Gallery block · WebGL2

↑ ↓ to move↵ to openesc to close