Web InnoventixFreeCode

Grid Reveal Scroll Gallery

Original · free

An image grid whose tiles fade and scale in, staggered, as they scroll into view.

byWeb InnoventixReact + Tailwind
gridrevealscrollgalleryimages
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/gallery-grid-reveal-scroll.json
gallery-grid-reveal-scroll.tsx
"use client";

import { useRef } from "react";
import { motion, useScroll, useTransform } from "motion/react";

type Orientation = "portrait" | "landscape" | "square";

interface Shot {
  src: string;
  alt: string;
  title: string;
  category: string;
  orientation: Orientation;
}

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g03.webp",
    alt: "Soft morning light falling across a quiet coastline",
    title: "Coastal light",
    category: "Landscape",
    orientation: "portrait",
  },
  {
    src: "/img/gallery/g11.webp",
    alt: "Overhead view of a minimal ceramics studio bench",
    title: "Studio 04",
    category: "Craft",
    orientation: "landscape",
  },
  {
    src: "/img/gallery/g18.webp",
    alt: "Detail of dew resting on a fern leaf at dawn",
    title: "First frost",
    category: "Nature",
    orientation: "square",
  },
  {
    src: "/img/gallery/g07.webp",
    alt: "A lone figure walking a wide empty beach",
    title: "Long shore",
    category: "Portrait",
    orientation: "portrait",
  },
  {
    src: "/img/gallery/g22.webp",
    alt: "Warm interior with sunlight raking across a plaster wall",
    title: "Afternoon room",
    category: "Interiors",
    orientation: "landscape",
  },
  {
    src: "/img/gallery/g14.webp",
    alt: "Close crop of layered mountain ridges in haze",
    title: "Blue ridges",
    category: "Landscape",
    orientation: "square",
  },
  {
    src: "/img/gallery/g29.webp",
    alt: "Hands folding fabric on a linen-covered table",
    title: "Slow craft",
    category: "Craft",
    orientation: "portrait",
  },
  {
    src: "/img/gallery/g05.webp",
    alt: "Golden field grasses bending in low evening wind",
    title: "Field study",
    category: "Nature",
    orientation: "landscape",
  },
  {
    src: "/img/gallery/g33.webp",
    alt: "Architectural stair detail catching hard midday shadow",
    title: "Concrete rhythm",
    category: "Architecture",
    orientation: "portrait",
  },
  {
    src: "/img/gallery/g26.webp",
    alt: "Still life of fruit and glass on a bright windowsill",
    title: "Windowsill",
    category: "Still life",
    orientation: "square",
  },
];

const SPAN: Record<Orientation, string> = {
  portrait: "sm:row-span-2",
  landscape: "sm:col-span-2",
  square: "",
};

const RATIO: Record<Orientation, string> = {
  portrait: "aspect-[4/5]",
  landscape: "aspect-[13/9]",
  square: "aspect-square",
};

export default function GalleryGridRevealScroll() {
  const headRef = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: headRef,
    offset: ["start end", "end start"],
  });
  const lineWidth = useTransform(scrollYProgress, [0, 0.6], ["0%", "100%"]);

  return (
    <section className="relative w-full overflow-hidden bg-white px-5 py-24 text-slate-900 sm:px-8 lg:py-32 dark:bg-neutral-950 dark:text-slate-100">
      <style>{`
        @keyframes ggrs-float {
          0%, 100% { transform: translate3d(0, 0, 0); }
          50% { transform: translate3d(0, -14px, 0); }
        }
        @media (prefers-reduced-motion: reduce) {
          .ggrs-orb { animation: none !important; }
        }
      `}</style>

      {/* ambient background */}
      <div
        aria-hidden
        className="ggrs-orb pointer-events-none absolute -top-32 right-[-10%] h-[26rem] w-[26rem] rounded-full bg-gradient-to-br from-indigo-300/40 via-violet-300/30 to-transparent blur-3xl dark:from-indigo-600/20 dark:via-violet-600/15"
        style={{ animation: "ggrs-float 11s ease-in-out infinite" }}
      />
      <div
        aria-hidden
        className="ggrs-orb pointer-events-none absolute bottom-[-8rem] left-[-8%] h-[22rem] w-[22rem] rounded-full bg-gradient-to-tr from-emerald-300/30 via-sky-300/20 to-transparent blur-3xl dark:from-emerald-600/15 dark:via-sky-600/10"
        style={{ animation: "ggrs-float 13s ease-in-out infinite reverse" }}
      />

      <div className="relative mx-auto max-w-6xl">
        {/* header */}
        <div ref={headRef} className="mx-auto max-w-2xl text-center">
          <motion.span
            initial={{ opacity: 0, y: 12 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true, amount: 0.6 }}
            transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
            className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-4 py-1.5 text-xs font-medium tracking-wide text-slate-600 uppercase dark:border-white/10 dark:bg-white/5 dark:text-slate-300"
          >
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Selected work
          </motion.span>

          <motion.h2
            initial={{ opacity: 0, y: 18 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true, amount: 0.6 }}
            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.05 }}
            className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl"
          >
            Frames that reveal
            <span className="bg-gradient-to-r from-indigo-500 via-violet-500 to-emerald-500 bg-clip-text text-transparent">
              {" "}
              as you look
            </span>
          </motion.h2>

          <motion.p
            initial={{ opacity: 0, y: 18 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true, amount: 0.6 }}
            transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.12 }}
            className="mx-auto mt-5 max-w-lg text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400"
          >
            A quiet collection of light, place, and craft. Each tile eases into
            view as it enters the frame.
          </motion.p>

          <motion.div
            style={{ width: lineWidth }}
            className="mx-auto mt-8 h-px max-w-xs bg-gradient-to-r from-transparent via-slate-300 to-transparent dark:via-white/20"
          />
        </div>

        {/* grid */}
        <div className="mt-14 grid auto-rows-[180px] grid-cols-1 gap-4 sm:grid-cols-2 sm:auto-rows-[200px] lg:grid-cols-3">
          {SHOTS.map((shot, i) => (
            <motion.figure
              key={shot.src}
              initial={{ opacity: 0, scale: 0.9, y: 28 }}
              whileInView={{ opacity: 1, scale: 1, y: 0 }}
              viewport={{ once: true, amount: 0.25, margin: "0px 0px -60px 0px" }}
              transition={{
                duration: 0.7,
                ease: [0.22, 1, 0.36, 1],
                delay: (i % 3) * 0.08,
              }}
              className={`group relative overflow-hidden rounded-2xl bg-slate-100 shadow-sm ring-1 ring-slate-900/5 dark:bg-neutral-900 dark:ring-white/10 ${SPAN[shot.orientation]}`}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={shot.src}
                alt={shot.alt}
                loading="lazy"
                draggable={false}
                className={`h-full w-full object-cover transition-transform duration-[900ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-[1.06] ${RATIO[shot.orientation]} sm:aspect-auto`}
              />

              {/* gradient scrim */}
              <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-950/70 via-slate-950/5 to-transparent opacity-80 transition-opacity duration-500 group-hover:opacity-95" />

              {/* category chip */}
              <figcaption className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-3 p-4">
                <div className="translate-y-1 transition-transform duration-500 group-hover:translate-y-0">
                  <p className="text-[10px] font-semibold tracking-[0.14em] text-white/70 uppercase">
                    {shot.category}
                  </p>
                  <p className="mt-0.5 text-base font-medium text-white drop-shadow-sm">
                    {shot.title}
                  </p>
                </div>
                <span
                  aria-hidden
                  className="mb-1 flex h-8 w-8 shrink-0 translate-y-2 items-center justify-center rounded-full bg-white/15 text-white opacity-0 backdrop-blur-sm transition-all duration-500 group-hover:translate-y-0 group-hover:opacity-100"
                >
                  <svg
                    viewBox="0 0 24 24"
                    fill="none"
                    className="h-4 w-4"
                    stroke="currentColor"
                    strokeWidth={2}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <path d="M7 17 17 7M9 7h8v8" />
                  </svg>
                </span>
              </figcaption>

              {/* focus/hover ring */}
              <span className="pointer-events-none absolute inset-0 rounded-2xl ring-2 ring-indigo-500/0 transition-[box-shadow,ring] duration-500 group-hover:ring-indigo-500/40" />
            </motion.figure>
          ))}
        </div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →
Hover Caption Gallery

Hover Caption Gallery

Original

A responsive image grid where each tile subtly zooms and reveals a caption bar on hover.

Masonry Gallery

Masonry Gallery

Original

A Pinterest-style masonry gallery using CSS columns with varied-height images; tiles lift and brighten on hover.

Filterable Gallery

Filterable Gallery

Original

A filterable gallery with category tabs; clicking a tab animates the grid to show only that category.

Tilt Cards Gallery

Tilt Cards Gallery

Original

A grid of image cards that tilt in 3D toward the cursor on hover, with a soft glare.

Lightbox Gallery

Lightbox Gallery

Original

A thumbnail grid where clicking a thumb opens a full-screen lightbox with prev/next arrows and keyboard support.

Expanding Strip Gallery

Expanding Strip Gallery

Original

A horizontal strip of image panels; hovering a panel expands it and shrinks the others to reveal its caption.

Marquee Gallery

Marquee Gallery

Original

Two rows of images auto-scrolling in opposite directions, pausing on hover, with edge fade masks.

Hover Zoom Overlay Gallery

Hover Zoom Overlay Gallery

Original

A grid where each image scales up under a dark gradient overlay revealing a title and a view button on hover.

Polaroid Stack Gallery

Polaroid Stack Gallery

Original

A scattered stack of slightly-rotated polaroid photos that straighten and lift on hover.

Featured Thumbs Gallery

Featured Thumbs Gallery

Original

A large featured image with a thumbnail row; selecting a thumbnail swaps the featured image with a crossfade.

Carousel Gallery

Carousel Gallery

Original

An image carousel with arrows and dots, smooth slide transitions and autoplay that pauses on hover.

Duotone Hover Gallery

Duotone Hover Gallery

Original

A grid where images sit under a coloured duotone wash that clears to full colour on hover.