Web InnoventixFreeCode

Fullbleed Rows Gallery

Original · free

Full-bleed horizontal image rows that grow taller on hover to reveal a caption and accent line.

byWeb InnoventixReact + Tailwind
fullbleedrowsgalleryimages
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-fullbleed-rows.json
gallery-fullbleed-rows.tsx
"use client";

import { useRef, useState } from "react";
import { motion, useInView } from "motion/react";

type Shot = {
  src: string;
  title: string;
  category: string;
  place: string;
  alt: string;
};

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g03.webp",
    title: "Coastal light",
    category: "Landscape",
    place: "Amalfi, IT",
    alt: "Warm morning light spilling across a rugged coastline",
  },
  {
    src: "/img/gallery/g11.webp",
    title: "Studio 04",
    category: "Portrait",
    place: "Berlin studio",
    alt: "Soft studio portrait framed against a plain backdrop",
  },
  {
    src: "/img/gallery/g18.webp",
    title: "Low tide",
    category: "Nature",
    place: "Outer Hebrides",
    alt: "Wet sand mirroring a pale overcast sky at low tide",
  },
  {
    src: "/img/gallery/g07.webp",
    title: "Concrete grammar",
    category: "Architecture",
    place: "Marseille",
    alt: "Repeating concrete forms lit by hard afternoon sun",
  },
  {
    src: "/img/gallery/g24.webp",
    title: "First frost",
    category: "Nature",
    place: "Dolomites",
    alt: "Frost gathering on grasses at the edge of an alpine meadow",
  },
  {
    src: "/img/gallery/g15.webp",
    title: "Neon hours",
    category: "Street",
    place: "Osaka",
    alt: "A quiet backstreet washed in colored neon at night",
  },
  {
    src: "/img/gallery/g29.webp",
    title: "Long exposure",
    category: "Abstract",
    place: "Studio",
    alt: "Streaks of colored light drawn across a dark abstract field",
  },
  {
    src: "/img/gallery/g33.webp",
    title: "The commute",
    category: "Documentary",
    place: "Lisbon",
    alt: "Silhouetted figures crossing a bright station concourse",
  },
];

export default function GalleryFullbleedRows() {
  const sectionRef = useRef<HTMLElement>(null);
  const inView = useInView(sectionRef, { once: true, margin: "-80px" });
  const [active, setActive] = useState<number | null>(null);

  return (
    <section
      ref={sectionRef}
      aria-label="Featured photography, full-bleed rows"
      className="relative w-full overflow-hidden bg-neutral-50 px-4 py-20 text-neutral-900 sm:px-6 lg:px-10 lg:py-28 dark:bg-neutral-950 dark:text-neutral-50"
    >
      <style>{`
        @keyframes gfr-rise {
          from { opacity: 0; transform: translateY(28px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        .gfr-row {
          transition: height 620ms cubic-bezier(0.16, 1, 0.3, 1),
                      border-radius 620ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .gfr-img {
          transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
                      filter 620ms ease;
        }
        .gfr-panel {
          transition: opacity 520ms ease, transform 620ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .gfr-rule {
          transform-origin: left center;
          transition: transform 640ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        @media (prefers-reduced-motion: reduce) {
          .gfr-row, .gfr-img, .gfr-panel, .gfr-rule { transition: none !important; }
          .gfr-fade { animation: none !important; opacity: 1 !important; transform: none !important; }
        }
      `}</style>

      <div className="relative mx-auto max-w-6xl">
        <motion.header
          initial={{ opacity: 0, y: 20 }}
          animate={inView ? { opacity: 1, y: 0 } : undefined}
          transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
          className="mb-12 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"
        >
          <div>
            <span className="inline-flex items-center gap-2 text-xs font-medium uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
              <span className="h-px w-6 bg-indigo-600 dark:bg-indigo-400" />
              Selected work
            </span>
            <h2 className="mt-4 text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
              Field notes in light
            </h2>
          </div>
          <p className="max-w-sm text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">
            Eight frames from the archive. Hover or focus a strip to let it
            breathe and read the story behind the shot.
          </p>
        </motion.header>

        <div className="flex flex-col gap-3">
          {SHOTS.map((shot, i) => {
            const isActive = active === i;
            return (
              <motion.button
                key={shot.src}
                type="button"
                aria-label={`${shot.title} — ${shot.category}, ${shot.place}`}
                onMouseEnter={() => setActive(i)}
                onMouseLeave={() => setActive((cur) => (cur === i ? null : cur))}
                onFocus={() => setActive(i)}
                onBlur={() => setActive((cur) => (cur === i ? null : cur))}
                initial={{ opacity: 0, y: 26 }}
                animate={inView ? { opacity: 1, y: 0 } : undefined}
                transition={{
                  duration: 0.7,
                  ease: [0.16, 1, 0.3, 1],
                  delay: 0.12 + i * 0.06,
                }}
                className={`gfr-row group relative block w-full overflow-hidden rounded-xl outline-none ring-indigo-500/70 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:ring-indigo-400/70 dark:focus-visible:ring-offset-neutral-950 ${
                  isActive
                    ? "h-72 rounded-2xl sm:h-80 lg:h-96"
                    : "h-24 sm:h-28"
                }`}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={shot.src}
                  alt={shot.alt}
                  loading="lazy"
                  draggable={false}
                  className={`gfr-img absolute inset-0 h-full w-full object-cover ${
                    isActive
                      ? "scale-100 brightness-100"
                      : "scale-105 brightness-90 dark:brightness-75"
                  }`}
                />

                {/* legibility wash */}
                <div
                  aria-hidden
                  className={`absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-black/10 transition-opacity duration-500 ${
                    isActive ? "opacity-100" : "opacity-70"
                  }`}
                />

                {/* index + category, always visible on the collapsed strip */}
                <div className="pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between px-5 py-3.5 text-white sm:px-7">
                  <span className="text-[0.7rem] font-medium uppercase tracking-[0.2em] text-white/70">
                    {shot.category}
                  </span>
                  <span className="font-mono text-[0.7rem] tabular-nums text-white/50">
                    {String(i + 1).padStart(2, "0")} / {String(SHOTS.length).padStart(2, "0")}
                  </span>
                </div>

                {/* expanded caption panel */}
                <div
                  className={`gfr-panel pointer-events-none absolute inset-x-0 bottom-0 px-5 pb-6 text-left text-white sm:px-7 sm:pb-8 ${
                    isActive
                      ? "translate-y-0 opacity-100"
                      : "translate-y-3 opacity-0"
                  }`}
                >
                  <div
                    className={`gfr-rule mb-4 h-px w-16 bg-indigo-400 ${
                      isActive ? "scale-x-100" : "scale-x-0"
                    }`}
                  />
                  <div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-1">
                    <h3 className="text-2xl font-semibold tracking-tight sm:text-3xl">
                      {shot.title}
                    </h3>
                    <span className="text-sm text-white/70">{shot.place}</span>
                  </div>
                </div>
              </motion.button>
            );
          })}
        </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.