Web InnoventixFreeCode

Split Diagonal Gallery

Original · free

A diagonal split panel that slides to reveal the full image on hover.

byWeb InnoventixReact + Tailwind
splitdiagonalgalleryimages
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-split-diagonal.json
gallery-split-diagonal.tsx
"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

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.