Web InnoventixFreeCode

Detail Slide Gallery

Original · free

A grid where hovering a tile slides a detail panel in over part of the image.

byWeb InnoventixReact + Tailwind
detailslidegalleryimages
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-detail-slide.json
gallery-detail-slide.tsx
"use client";

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

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

interface Photo {
  id: number;
  src: string;
  title: string;
  category: string;
  location: string;
  year: string;
  spec: string;
  alt: string;
  orientation: Orientation;
}

const PHOTOS: Photo[] = [
  {
    id: 1,
    src: "/img/gallery/g03.webp",
    title: "Morning Over the Dolomites",
    category: "Landscape",
    location: "Val Gardena, Italy",
    year: "2024",
    spec: "35mm · f/8 · 1/250s",
    alt: "Sunrise light spilling across jagged Dolomite peaks above a green alpine valley",
    orientation: "portrait",
  },
  {
    id: 2,
    src: "/img/gallery/g11.webp",
    title: "Concrete Cathedral",
    category: "Architecture",
    location: "Osaka, Japan",
    year: "2023",
    spec: "24mm · f/11 · 1/60s",
    alt: "Interior of a brutalist concrete chapel with a slanted beam of daylight",
    orientation: "landscape",
  },
  {
    id: 3,
    src: "/img/gallery/g07.webp",
    title: "Saffron Harvest",
    category: "Documentary",
    location: "Pampore, Kashmir",
    year: "2024",
    spec: "50mm · f/2.8 · 1/500s",
    alt: "Hands cradling freshly picked purple saffron crocus flowers at dawn",
    orientation: "square",
  },
  {
    id: 4,
    src: "/img/gallery/g18.webp",
    title: "Tidal Geometry",
    category: "Aerial",
    location: "Wadden Sea, Netherlands",
    year: "2023",
    spec: "DJI · f/4 · 1/800s",
    alt: "Overhead view of branching tidal channels carved into wet coastal mudflats",
    orientation: "landscape",
  },
  {
    id: 5,
    src: "/img/gallery/g22.webp",
    title: "Neon Rain",
    category: "Street",
    location: "Myeongdong, Seoul",
    year: "2025",
    spec: "35mm · f/1.4 · 1/125s",
    alt: "Wet night street glowing with reflected neon signage and passing umbrellas",
    orientation: "portrait",
  },
  {
    id: 6,
    src: "/img/gallery/g14.webp",
    title: "Salt Flat Mirror",
    category: "Landscape",
    location: "Salar de Uyuni, Bolivia",
    year: "2022",
    spec: "20mm · f/9 · 1/320s",
    alt: "A thin layer of water on a vast salt flat mirroring a pastel sky",
    orientation: "landscape",
  },
  {
    id: 7,
    src: "/img/gallery/g05.webp",
    title: "Portrait in Ochre",
    category: "Portrait",
    location: "Marrakech, Morocco",
    year: "2024",
    spec: "85mm · f/2 · 1/400s",
    alt: "Portrait of an elder against a warm ochre wall in soft afternoon light",
    orientation: "portrait",
  },
  {
    id: 8,
    src: "/img/gallery/g27.webp",
    title: "Ferns at First Light",
    category: "Nature",
    location: "Olympic NP, USA",
    year: "2023",
    spec: "60mm macro · f/5.6 · 1/160s",
    alt: "Backlit unfurling fern fronds glowing green in early morning forest light",
    orientation: "square",
  },
  {
    id: 9,
    src: "/img/gallery/g31.webp",
    title: "Aurora Field",
    category: "Astro",
    location: "Lofoten, Norway",
    year: "2025",
    spec: "14mm · f/2.8 · 8s",
    alt: "Green aurora borealis arcing over snow-dusted peaks and a still fjord",
    orientation: "landscape",
  },
  {
    id: 10,
    src: "/img/gallery/g09.webp",
    title: "The Long Room",
    category: "Interiors",
    location: "Trinity College, Dublin",
    year: "2022",
    spec: "16mm · f/6.3 · 1/50s",
    alt: "Barrel-vaulted library hall lined with towering shelves of old books",
    orientation: "portrait",
  },
];

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

export default function GalleryDetailSlide() {
  const headRef = useRef<HTMLDivElement>(null);
  const headInView = useInView(headRef, { once: true, margin: "-10% 0px" });

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 py-20 text-neutral-900 sm:py-28 dark:bg-neutral-950 dark:text-neutral-50">
      <style>{`
        @keyframes gds-sheen {
          0%   { transform: translateX(-130%) skewX(-12deg); opacity: 0; }
          22%  { opacity: 0.55; }
          100% { transform: translateX(240%) skewX(-12deg); opacity: 0; }
        }
        @keyframes gds-nudge {
          0%, 100% { transform: translateX(0); }
          50%      { transform: translateX(4px); }
        }
        .gds-tile:hover .gds-sheen,
        .gds-tile:focus-within .gds-sheen {
          animation: gds-sheen 0.9s ease-out;
        }
        .gds-tile:hover .gds-arrow,
        .gds-tile:focus-within .gds-arrow {
          animation: gds-nudge 1.1s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .gds-tile:hover .gds-sheen,
          .gds-tile:focus-within .gds-sheen,
          .gds-tile:hover .gds-arrow,
          .gds-tile:focus-within .gds-arrow {
            animation: none;
          }
        }
      `}</style>

      {/* ambient background wash */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 -z-10 opacity-70 [background:radial-gradient(60rem_40rem_at_15%_-10%,theme(colors.indigo.200/0.55),transparent_60%),radial-gradient(50rem_35rem_at_100%_10%,theme(colors.violet.200/0.45),transparent_55%)] dark:opacity-40 dark:[background:radial-gradient(60rem_40rem_at_15%_-10%,theme(colors.indigo.500/0.22),transparent_60%),radial-gradient(50rem_35rem_at_100%_10%,theme(colors.violet.500/0.18),transparent_55%)]"
      />

      <div className="mx-auto w-full max-w-7xl px-5 sm:px-8">
        <motion.div
          ref={headRef}
          initial={{ opacity: 0, y: 24 }}
          animate={headInView ? { opacity: 1, y: 0 } : undefined}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
          className="mb-12 flex flex-col gap-5 sm:mb-16 sm:flex-row sm:items-end sm:justify-between"
        >
          <div className="max-w-2xl">
            <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 uppercase backdrop-blur dark:border-indigo-400/30 dark:bg-white/5 dark:text-indigo-300">
              <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
              Field Archive · Vol. 07
            </span>
            <h2 className="mt-5 text-3xl font-semibold tracking-tight text-balance sm:text-5xl">
              Ten frames from the road
            </h2>
            <p className="mt-4 text-base leading-relaxed text-neutral-600 sm:text-lg dark:text-neutral-400">
              A rotating selection of work shot on assignment across four
              continents. Hover or focus any frame to reveal where it was made.
            </p>
          </div>
          <div className="shrink-0 text-sm text-neutral-500 dark:text-neutral-500">
            <span className="font-mono text-neutral-900 dark:text-neutral-100">
              10
            </span>{" "}
            selected · 2022–2025
          </div>
        </motion.div>

        <div className="[column-gap:1.25rem] sm:columns-2 lg:columns-3">
          {PHOTOS.map((photo, i) => (
            <motion.article
              key={photo.id}
              initial={{ opacity: 0, y: 28 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true, margin: "-8% 0px" }}
              transition={{
                duration: 0.55,
                ease: [0.22, 1, 0.36, 1],
                delay: (i % 3) * 0.06,
              }}
              className="gds-tile group relative mb-5 block break-inside-avoid overflow-hidden rounded-2xl bg-neutral-200 shadow-sm ring-1 ring-black/5 transition-shadow duration-300 hover:shadow-xl focus-within:shadow-xl dark:bg-neutral-800 dark:ring-white/10"
            >
              <div className={`relative w-full ${ASPECT[photo.orientation]}`}>
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={photo.src}
                  alt={photo.alt}
                  loading="lazy"
                  draggable={false}
                  className="h-full w-full scale-100 object-cover transition-transform duration-[900ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-[1.06] group-focus-within:scale-[1.06]"
                />

                {/* base darkening for legibility of the always-on index/chip */}
                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/45 via-transparent to-black/25 opacity-90 transition-opacity duration-500 group-hover:opacity-100"
                />

                {/* diagonal sheen sweep on hover/focus */}
                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-0 overflow-hidden"
                >
                  <div className="gds-sheen absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/60 to-transparent opacity-0" />
                </div>

                {/* persistent index + category */}
                <div className="pointer-events-none absolute inset-x-0 top-0 flex items-start justify-between p-4">
                  <span className="rounded-full bg-black/45 px-2.5 py-1 text-[11px] font-medium tracking-wider text-white uppercase backdrop-blur-sm">
                    {photo.category}
                  </span>
                  <span className="font-mono text-xs text-white/80 tabular-nums">
                    {String(photo.id).padStart(2, "0")}
                    <span className="text-white/40"> / 10</span>
                  </span>
                </div>

                {/* the sliding detail panel */}
                <div className="pointer-events-none absolute inset-x-0 bottom-0 translate-y-full transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0 group-focus-within:translate-y-0">
                  <div className="bg-gradient-to-t from-black/85 via-black/70 to-transparent px-5 pt-10 pb-5">
                    <h3 className="text-lg font-semibold text-white text-balance">
                      {photo.title}
                    </h3>
                    <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-white/75">
                      <span>{photo.location}</span>
                      <span aria-hidden className="text-white/35">
                        •
                      </span>
                      <span>{photo.year}</span>
                    </div>
                    <div className="mt-3 flex items-center justify-between border-t border-white/15 pt-3">
                      <span className="font-mono text-[11px] tracking-wide text-white/60">
                        {photo.spec}
                      </span>
                      <span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-300">
                        View frame
                        <svg
                          viewBox="0 0 16 16"
                          className="gds-arrow h-3.5 w-3.5"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={1.75}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                          aria-hidden
                        >
                          <path d="M3 8h9M8.5 4l4 4-4 4" />
                        </svg>
                      </span>
                    </div>
                  </div>
                </div>

                {/* single interactive control: keyboard focus reveals the panel */}
                <button
                  type="button"
                  aria-label={`${photo.title} — ${photo.category}, ${photo.location}, ${photo.year}`}
                  className="absolute inset-0 z-10 cursor-pointer rounded-2xl outline-none ring-inset transition-[box-shadow] focus-visible:ring-2 focus-visible:ring-indigo-400 dark:focus-visible:ring-indigo-300"
                />
              </div>
            </motion.article>
          ))}
        </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.