Web InnoventixFreeCode

Zoom Caption Effect

Original · free

Image zooms while a caption fades up on hover.

byWeb InnoventixReact + Tailwind
zoomcaptionimageeffect
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/imgfx-zoom-caption.json
imgfx-zoom-caption.tsx
"use client";

import { useId } from "react";

type Shot = {
  src: string;
  alt: string;
  kicker: string;
  caption: string;
};

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit ridge line fading into layered morning haze",
    kicker: "Field notes",
    caption: "Ridge lines soften the higher you climb.",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Quiet city street washed in low evening light",
    kicker: "After hours",
    caption: "The street empties and the light gets honest.",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Close detail of textured natural surface under warm light",
    kicker: "Up close",
    caption: "Every surface has a grain worth slowing down for.",
  },
];

export default function ImgfxZoomCaption() {
  const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
  const prefix = `zc${uid}`;

  return (
    <section className="relative w-full bg-white px-6 py-20 text-slate-900 sm:px-10 sm:py-28 dark:bg-neutral-950 dark:text-slate-100">
      <style>{`
        .${prefix}-card { --zc-dur: 620ms; --zc-ease: cubic-bezier(0.22, 1, 0.36, 1); }
        .${prefix}-img {
          transform: scale(1);
          transition: transform var(--zc-dur) var(--zc-ease), filter var(--zc-dur) var(--zc-ease);
          will-change: transform;
        }
        .${prefix}-card:hover .${prefix}-img,
        .${prefix}-card:focus-visible .${prefix}-img {
          transform: scale(1.12);
        }
        .${prefix}-veil {
          opacity: 0;
          transition: opacity var(--zc-dur) var(--zc-ease);
        }
        .${prefix}-card:hover .${prefix}-veil,
        .${prefix}-card:focus-visible .${prefix}-veil { opacity: 1; }
        .${prefix}-cap {
          opacity: 0;
          transform: translateY(18px);
          transition: opacity var(--zc-dur) var(--zc-ease), transform var(--zc-dur) var(--zc-ease);
        }
        .${prefix}-card:hover .${prefix}-cap,
        .${prefix}-card:focus-visible .${prefix}-cap {
          opacity: 1;
          transform: translateY(0);
        }
        .${prefix}-kick {
          opacity: 0;
          transform: translateY(10px);
          transition: opacity var(--zc-dur) var(--zc-ease), transform var(--zc-dur) var(--zc-ease);
          transition-delay: 70ms;
        }
        .${prefix}-card:hover .${prefix}-kick,
        .${prefix}-card:focus-visible .${prefix}-kick {
          opacity: 1;
          transform: translateY(0);
        }
        @media (prefers-reduced-motion: reduce) {
          .${prefix}-img,
          .${prefix}-veil,
          .${prefix}-cap,
          .${prefix}-kick {
            transition-duration: 1ms !important;
            transition-delay: 0ms !important;
          }
          .${prefix}-card:hover .${prefix}-img,
          .${prefix}-card:focus-visible .${prefix}-img { transform: scale(1.04); }
          .${prefix}-cap { transform: none; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <div className="mb-12 max-w-2xl">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Zoom &amp; Caption
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            The image leans in, the story fades up
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-400">
            A slow, weighted zoom paired with a caption that rises from the
            frame. Hover any photo, or tab to it, to reveal the line.
          </p>
        </div>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {SHOTS.map((shot, i) => (
            <figure
              key={shot.src}
              tabIndex={0}
              aria-label={`${shot.kicker}: ${shot.caption}`}
              className={`${prefix}-card group relative aspect-[4/5] cursor-pointer overflow-hidden rounded-2xl bg-slate-100 shadow-sm outline-none ring-1 ring-slate-900/5 transition-shadow duration-500 hover:shadow-xl focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-900 dark:ring-white/10 ${
                i === 2 ? "sm:col-span-2 lg:col-span-1" : ""
              }`}
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={shot.src}
                alt={shot.alt}
                loading="lazy"
                draggable={false}
                className={`${prefix}-img absolute inset-0 h-full w-full object-cover`}
              />

              <div
                className={`${prefix}-veil pointer-events-none absolute inset-0 bg-gradient-to-t from-slate-950/85 via-slate-950/25 to-transparent`}
                aria-hidden="true"
              />

              <figcaption className="pointer-events-none absolute inset-x-0 bottom-0 p-5 sm:p-6">
                <span
                  className={`${prefix}-kick mb-2 inline-block text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-indigo-200`}
                >
                  {shot.kicker}
                </span>
                <p
                  className={`${prefix}-cap text-lg font-medium leading-snug text-white`}
                >
                  {shot.caption}
                </p>
              </figcaption>
            </figure>
          ))}
        </div>

        <p className="mt-8 text-sm text-slate-500 dark:text-slate-500">
          Hover or focus a frame to reveal its caption.
        </p>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 →