Split Diagonal Gallery
Original · freeA diagonal split panel that slides to reveal the full image on hover.
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-split-diagonal.json"use client";
import { useRef } from "react";
import { motion, useInView } from "motion/react";
type Accent = "indigo" | "violet" | "emerald" | "rose" | "amber";
interface GalleryItem {
src: string;
alt: string;
title: string;
category: string;
place: string;
accent: Accent;
}
const ACCENTS: Record<
Accent,
{ panel: string; seam: string; ring: string; chip: string; dot: string }
> = {
indigo: {
panel: "from-indigo-950/85 via-indigo-900/55 to-indigo-800/25",
seam: "from-indigo-300 via-white to-indigo-400",
ring: "focus-visible:ring-indigo-400/70",
chip: "bg-indigo-500/20 text-indigo-50 ring-indigo-300/40",
dot: "bg-indigo-300",
},
violet: {
panel: "from-violet-950/85 via-violet-900/55 to-violet-800/25",
seam: "from-violet-300 via-white to-violet-400",
ring: "focus-visible:ring-violet-400/70",
chip: "bg-violet-500/20 text-violet-50 ring-violet-300/40",
dot: "bg-violet-300",
},
emerald: {
panel: "from-emerald-950/85 via-emerald-900/55 to-emerald-800/25",
seam: "from-emerald-300 via-white to-emerald-400",
ring: "focus-visible:ring-emerald-400/70",
chip: "bg-emerald-500/20 text-emerald-50 ring-emerald-300/40",
dot: "bg-emerald-300",
},
rose: {
panel: "from-rose-950/85 via-rose-900/55 to-rose-800/25",
seam: "from-rose-300 via-white to-rose-400",
ring: "focus-visible:ring-rose-400/70",
chip: "bg-rose-500/20 text-rose-50 ring-rose-300/40",
dot: "bg-rose-300",
},
amber: {
panel: "from-amber-950/85 via-amber-900/55 to-amber-800/25",
seam: "from-amber-300 via-white to-amber-400",
ring: "focus-visible:ring-amber-400/70",
chip: "bg-amber-500/20 text-amber-50 ring-amber-300/40",
dot: "bg-amber-300",
},
};
const ITEMS: GalleryItem[] = [
{
src: "/img/gallery/g04.webp",
alt: "Cantilevered concrete house glowing at dusk over a still reflecting pool",
title: "House on the Ledge",
category: "Architecture",
place: "Big Sur, CA",
accent: "indigo",
},
{
src: "/img/gallery/g11.webp",
alt: "Morning fog rolling between dark pine ridges in a mountain valley",
title: "Valley of the Long Fog",
category: "Landscape",
place: "Dolomites, IT",
accent: "emerald",
},
{
src: "/img/gallery/g17.webp",
alt: "Ceramicist's hands shaping a wet clay bowl on a spinning wheel",
title: "Twelve Bowls Before Noon",
category: "Craft",
place: "Kyoto, JP",
accent: "amber",
},
{
src: "/img/gallery/g22.webp",
alt: "Neon-lit rain-slicked alley reflecting signage in colored puddles",
title: "After the Rain, Sign No. 9",
category: "Street",
place: "Osaka, JP",
accent: "violet",
},
{
src: "/img/gallery/g08.webp",
alt: "Portrait of a dancer mid-turn, skirt caught in warm studio light",
title: "Turn, Held",
category: "Portrait",
place: "Studio 3, Lisbon",
accent: "rose",
},
{
src: "/img/gallery/g29.webp",
alt: "Sunlit spiral staircase seen from directly below inside a stone tower",
title: "Descent in Spirals",
category: "Interior",
place: "Porto, PT",
accent: "indigo",
},
{
src: "/img/gallery/g14.webp",
alt: "Windswept dunes at golden hour with sharp ridgelines of sand",
title: "Where the Wind Writes",
category: "Landscape",
place: "Erg Chebbi, MA",
accent: "amber",
},
{
src: "/img/gallery/g33.webp",
alt: "Overhead view of a market table covered in ripe summer produce",
title: "Saturday, Stall 14",
category: "Still Life",
place: "Provence, FR",
accent: "emerald",
},
];
export default function GallerySplitDiagonal() {
const sectionRef = useRef<HTMLElement>(null);
const inView = useInView(sectionRef, { once: true, margin: "-80px" });
return (
<section
ref={sectionRef}
className="relative w-full overflow-hidden bg-zinc-50 px-6 py-20 sm:px-10 sm:py-28 dark:bg-zinc-950"
aria-labelledby="gsd-heading"
>
<style>{`
@keyframes gsd-seam-pulse {
0%, 100% { opacity: 0.55; }
50% { opacity: 1; }
}
.gsd-seam { animation: gsd-seam-pulse 3.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.gsd-seam { animation: none !important; }
.gsd-panel, .gsd-img, .gsd-reveal { transition: none !important; }
}
`}</style>
{/* ambient backdrop */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.06] dark:opacity-[0.09]"
style={{
backgroundImage:
"repeating-linear-gradient(45deg, currentColor 0 1px, transparent 1px 22px)",
color: "#6366f1",
}}
/>
<div className="relative mx-auto max-w-7xl">
<header className="mb-14 max-w-2xl">
<span className="inline-flex items-center gap-2 rounded-full bg-zinc-900/5 px-3 py-1 text-xs font-medium uppercase tracking-[0.2em] text-zinc-600 ring-1 ring-zinc-900/10 dark:bg-white/5 dark:text-zinc-300 dark:ring-white/10">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Field Notes
</span>
<h2
id="gsd-heading"
className="mt-5 text-balance text-3xl font-semibold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-50"
>
Eight frames, cut on the diagonal
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-zinc-600 sm:text-lg dark:text-zinc-400">
Hover or focus any frame and the split panel slides away along the
seam, revealing the full photograph underneath.
</p>
</header>
<ul className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{ITEMS.map((item, i) => {
const a = ACCENTS[item.accent];
return (
<motion.li
key={item.src}
initial={{ opacity: 0, y: 28 }}
animate={inView ? { opacity: 1, y: 0 } : undefined}
transition={{
duration: 0.6,
delay: i * 0.07,
ease: [0.22, 1, 0.36, 1],
}}
>
<button
type="button"
aria-label={`${item.title} — ${item.category}, ${item.place}`}
className={`group relative block aspect-[4/5] w-full overflow-hidden rounded-2xl bg-zinc-200 outline-none ring-1 ring-zinc-900/10 transition-shadow duration-300 focus-visible:ring-2 hover:shadow-2xl hover:shadow-zinc-900/20 dark:bg-zinc-800 dark:ring-white/10 ${a.ring}`}
>
{/* underlying full image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.src}
alt={item.alt}
loading="lazy"
draggable={false}
className="gsd-img absolute inset-0 h-full w-full scale-105 object-cover transition-transform duration-[900ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-100 group-focus-visible:scale-100"
/>
{/* top-left triangle panel */}
<div
className={`gsd-panel absolute inset-0 z-10 translate-x-0 translate-y-0 bg-gradient-to-br ${a.panel} backdrop-blur-[1px] transition-transform duration-[750ms] ease-[cubic-bezier(0.65,0,0.35,1)] will-change-transform group-hover:[transform:translate(-112%,-112%)] group-focus-visible:[transform:translate(-112%,-112%)]`}
style={{ clipPath: "polygon(0 0, 100% 0, 0 100%)" }}
>
<div className="absolute left-5 top-5">
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-medium uppercase tracking-widest ring-1 backdrop-blur-sm ${a.chip}`}
>
<span className={`h-1 w-1 rounded-full ${a.dot}`} />
{item.category}
</span>
</div>
</div>
{/* bottom-right triangle panel */}
<div
className={`gsd-panel absolute inset-0 z-10 translate-x-0 translate-y-0 bg-gradient-to-tl ${a.panel} backdrop-blur-[1px] transition-transform duration-[750ms] ease-[cubic-bezier(0.65,0,0.35,1)] will-change-transform group-hover:[transform:translate(112%,112%)] group-focus-visible:[transform:translate(112%,112%)]`}
style={{ clipPath: "polygon(100% 0, 100% 100%, 0 100%)" }}
>
<div className="absolute bottom-5 right-5 text-right">
<h3 className="text-lg font-semibold leading-tight text-white drop-shadow-sm">
{item.title}
</h3>
<p className="mt-1 text-xs font-medium uppercase tracking-widest text-white/70">
{item.place}
</p>
</div>
</div>
{/* glowing seam that rides the anti-diagonal */}
<div
aria-hidden
className={`gsd-seam pointer-events-none absolute inset-0 z-20 bg-gradient-to-tr ${a.seam} transition-opacity duration-500 group-hover:opacity-0 group-focus-visible:opacity-0`}
style={{
clipPath:
"polygon(100% 0, 100% 2.4%, 2.4% 100%, 0 100%, 0 97.6%, 97.6% 0)",
mixBlendMode: "screen",
}}
/>
{/* revealed caption bar */}
<div className="gsd-reveal pointer-events-none absolute inset-x-0 bottom-0 z-30 translate-y-3 bg-gradient-to-t from-black/70 via-black/25 to-transparent px-5 pb-4 pt-10 opacity-0 transition-all duration-500 delay-150 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100">
<div className="flex items-end justify-between gap-3">
<span className="text-sm font-semibold text-white">
{item.title}
</span>
<span className="shrink-0 text-[11px] font-medium uppercase tracking-widest text-white/75">
{item.category}
</span>
</div>
</div>
</button>
</motion.li>
);
})}
</ul>
</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.

