Web InnoventixFreeCode

Rounded Lift Gallery

Original · free

Rounded image cards that lift with a soft shadow and reveal a label on hover.

byWeb InnoventixReact + Tailwind
roundedliftgalleryimages
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-rounded-lift.json
gallery-rounded-lift.tsx
"use client";

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

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

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g03.webp",
    alt: "Morning fog drifting between dark evergreen ridgelines",
    title: "Ridgeline at First Light",
    category: "Landscape",
    location: "Cascade Range, Oregon",
  },
  {
    src: "/img/gallery/g11.webp",
    alt: "Close portrait of a ceramicist's clay-dusted hands centering a pot",
    title: "Centering the Clay",
    category: "Craft",
    location: "Studio No. 4, Lisbon",
  },
  {
    src: "/img/gallery/g07.webp",
    alt: "Neon signage reflected on a rain-soaked side street at night",
    title: "Wet Neon, Empty Street",
    category: "Street",
    location: "Shinjuku, Tokyo",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Overhead flat lay of espresso, a notebook and dried orange slices",
    title: "The Nine A.M. Ritual",
    category: "Still Life",
    location: "Corner table, Turin",
  },
  {
    src: "/img/gallery/g22.webp",
    alt: "Sunlit dunes carved into long shadow-lined curves at dusk",
    title: "Shadow & Sand",
    category: "Landscape",
    location: "Erg Chebbi, Morocco",
  },
  {
    src: "/img/gallery/g14.webp",
    alt: "Portrait of a dancer mid-turn with fabric caught in motion",
    title: "Between Two Counts",
    category: "Portrait",
    location: "Rehearsal hall, Paris",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Spiral concrete staircase photographed straight down the well",
    title: "Descent",
    category: "Architecture",
    location: "Vatican Museums, Rome",
  },
  {
    src: "/img/gallery/g05.webp",
    alt: "Fishing boats moored in still turquoise water at golden hour",
    title: "Low Tide, No Wind",
    category: "Travel",
    location: "Kotor Bay, Montenegro",
  },
  {
    src: "/img/gallery/g31.webp",
    alt: "Macro of morning dew beading along the edge of a green leaf",
    title: "Overnight Rain",
    category: "Macro",
    location: "Backyard, Kyoto",
  },
];

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

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 py-20 sm:py-28 dark:bg-neutral-950">
      <style>{`
        .grl-card {
          animation: grl-fade-up 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
        }
        @keyframes grl-fade-up {
          from { opacity: 0; transform: translateY(26px) scale(0.985); }
          to   { opacity: 1; transform: translateY(0) scale(1); }
        }
        @media (prefers-reduced-motion: reduce) {
          .grl-card { animation: none !important; }
        }
      `}</style>

      {/* soft ambient wash */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 -z-10 opacity-70"
      >
        <div className="absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-200/40 blur-3xl dark:bg-indigo-500/10" />
        <div className="absolute -right-16 bottom-0 h-80 w-80 rounded-full bg-emerald-200/40 blur-3xl dark:bg-emerald-500/10" />
      </div>

      <div className="mx-auto w-full max-w-7xl px-5 sm:px-8">
        <div
          ref={headRef}
          className="mx-auto mb-12 max-w-2xl text-center sm:mb-16"
          style={{
            opacity: headInView ? 1 : 0,
            transform: headInView ? "translateY(0)" : "translateY(18px)",
            transition:
              "opacity 0.7s cubic-bezier(0.22,1,0.36,1), transform 0.7s cubic-bezier(0.22,1,0.36,1)",
          }}
        >
          <span className="inline-flex items-center gap-2 rounded-full border border-neutral-200 bg-white/70 px-3.5 py-1.5 text-xs font-medium tracking-wide text-neutral-600 shadow-sm backdrop-blur dark:border-neutral-800 dark:bg-neutral-900/70 dark:text-neutral-400">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Field Notes 2026
          </span>
          <h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl md:text-5xl dark:text-neutral-50">
            A year, in nine frames
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-neutral-600 sm:text-lg dark:text-neutral-400">
            Hand-picked stills from twelve months of wandering. Hover any card to
            read where it was made.
          </p>
        </div>

        <ul
          role="list"
          className="columns-1 gap-5 [column-fill:_balance] sm:columns-2 sm:gap-6 lg:columns-3"
        >
          {SHOTS.map((shot, i) => (
            <li key={shot.src} className="mb-5 sm:mb-6 break-inside-avoid">
              <motion.figure
                tabIndex={0}
                aria-label={`${shot.title} — ${shot.category}, ${shot.location}`}
                className="grl-card group relative block cursor-pointer overflow-hidden rounded-[1.4rem] bg-neutral-200 shadow-[0_1px_2px_rgba(15,23,42,0.06)] outline-none ring-1 ring-neutral-900/5 transition-[transform,box-shadow] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-transform hover:-translate-y-2 hover:shadow-[0_28px_50px_-18px_rgba(15,23,42,0.35)] focus-visible:-translate-y-2 focus-visible:shadow-[0_28px_50px_-18px_rgba(15,23,42,0.35)] focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10 dark:hover:shadow-[0_28px_55px_-18px_rgba(0,0,0,0.75)] dark:focus-visible:shadow-[0_28px_55px_-18px_rgba(0,0,0,0.75)]"
                style={{ animationDelay: `${i * 70}ms` }}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={shot.src}
                  alt={shot.alt}
                  loading="lazy"
                  draggable={false}
                  className="block w-full select-none object-cover transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-[1.06] group-focus-visible:scale-[1.06]"
                />

                {/* gradient scrim, fades in on hover */}
                <div
                  aria-hidden="true"
                  className="pointer-events-none absolute inset-0 bg-gradient-to-t from-neutral-950/80 via-neutral-950/10 to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-visible:opacity-100"
                />

                {/* category chip, top-left */}
                <span className="pointer-events-none absolute left-3.5 top-3.5 translate-y-1 rounded-full bg-white/85 px-2.5 py-1 text-[0.68rem] font-semibold uppercase tracking-wider text-neutral-800 opacity-0 shadow-sm backdrop-blur transition-all duration-500 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 dark:bg-neutral-900/85 dark:text-neutral-100">
                  {shot.category}
                </span>

                {/* label reveal, bottom */}
                <figcaption className="pointer-events-none absolute inset-x-0 bottom-0 translate-y-3 p-4 opacity-0 transition-all duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:translate-y-0 group-focus-visible:opacity-100 sm:p-5">
                  <h3 className="text-lg font-semibold tracking-tight text-white drop-shadow-sm">
                    {shot.title}
                  </h3>
                  <p className="mt-0.5 flex items-center gap-1.5 text-sm text-neutral-200">
                    <svg
                      viewBox="0 0 24 24"
                      className="h-3.5 w-3.5 shrink-0"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.8}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden="true"
                    >
                      <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
                      <circle cx="12" cy="10" r="3" />
                    </svg>
                    {shot.location}
                  </p>
                </figcaption>
              </motion.figure>
            </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.