Web InnoventixFreeCode

Zoom Frame Effect

Original · free

Image zooms inside an inset frame that draws in on hover.

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

const photos = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit canyon walls carved by a winding river",
    caption: "Canyon light",
  },
  {
    src: "/img/gallery/g18.webp",
    alt: "Quiet coastline at dusk with long reflections",
    caption: "Low tide",
  },
  {
    src: "/img/gallery/g25.webp",
    alt: "Dense forest canopy seen from below",
    caption: "Understory",
  },
];

export default function ImgfxZoomFrame() {
  return (
    <section className="relative w-full bg-neutral-50 px-6 py-20 dark:bg-neutral-950 sm:px-8 sm:py-28">
      <style>{`
        @keyframes zfr-fade-up {
          from { opacity: 0; transform: translateY(14px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .zfr-card {
          animation: zfr-fade-up 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
        }
        .zfr-frame {
          stroke-dasharray: 100;
          stroke-dashoffset: 100;
          transition: stroke-dashoffset 0.7s cubic-bezier(0.65, 0, 0.35, 1);
        }
        .group:hover .zfr-frame,
        .group:focus-within .zfr-frame {
          stroke-dashoffset: 0;
        }
        .zfr-img {
          transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1),
            filter 0.7s ease;
          transform: scale(1.001);
        }
        .group:hover .zfr-img,
        .group:focus-within .zfr-img {
          transform: scale(1.14);
          filter: saturate(1.08) contrast(1.03);
        }
        @media (prefers-reduced-motion: reduce) {
          .zfr-card { animation: none; }
          .zfr-frame { stroke-dashoffset: 0; transition: none; }
          .zfr-img { transition: none; transform: none; }
          .group:hover .zfr-img,
          .group:focus-within .zfr-img { transform: none; filter: none; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <div className="mb-14 max-w-2xl">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Image effect / Zoom frame
          </p>
          <h2 className="text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
            The photo zooms while an inset frame draws itself in
          </h2>
          <p className="mt-4 text-base text-neutral-600 dark:text-neutral-400">
            Hover any image. The picture eases into a slow zoom while a thin
            border traces around an inset margin, framing the shot as it moves.
          </p>
        </div>

        <ul className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
          {photos.map((photo, i) => (
            <li
              key={photo.src}
              className="zfr-card"
              style={{ animationDelay: `${i * 0.12}s` }}
            >
              <figure
                tabIndex={0}
                className="group relative block aspect-[4/5] w-full cursor-pointer overflow-hidden rounded-2xl bg-neutral-200 shadow-sm outline-none ring-1 ring-neutral-900/5 transition-shadow duration-500 hover:shadow-xl focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10"
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={photo.src}
                  alt={photo.alt}
                  loading="lazy"
                  draggable={false}
                  className="zfr-img absolute inset-0 h-full w-full select-none object-cover"
                />

                <svg
                  className="pointer-events-none absolute inset-0 h-full w-full"
                  viewBox="0 0 100 100"
                  preserveAspectRatio="none"
                  aria-hidden="true"
                >
                  <rect
                    className="zfr-frame"
                    x="8"
                    y="8"
                    width="84"
                    height="84"
                    rx="3"
                    pathLength={100}
                    fill="none"
                    stroke="white"
                    strokeWidth="0.9"
                    vectorEffect="non-scaling-stroke"
                  />
                </svg>

                <div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent p-5">
                  <figcaption className="text-sm font-medium text-white">
                    {photo.caption}
                  </figcaption>
                </div>
              </figure>
            </li>
          ))}
        </ul>

        <p className="mt-8 text-sm text-neutral-500 dark:text-neutral-500">
          Hover or focus to reveal the frame.
        </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 →