Detail Slide Gallery
Original · freeA grid where hovering a tile slides a detail panel in over part of the image.
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"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
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 quoteSimilar components
Browse all →
Hover Caption Gallery
OriginalA responsive image grid where each tile subtly zooms and reveals a caption bar on hover.

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

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

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

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

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

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

Hover Zoom Overlay Gallery
OriginalA grid where each image scales up under a dark gradient overlay revealing a title and a view button on hover.

Polaroid Stack Gallery
OriginalA scattered stack of slightly-rotated polaroid photos that straighten and lift on hover.
Featured Thumbs Gallery
OriginalA large featured image with a thumbnail row; selecting a thumbnail swaps the featured image with a crossfade.

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

Duotone Hover Gallery
OriginalA grid where images sit under a coloured duotone wash that clears to full colour on hover.

