Web InnoventixFreeCode

Zoom On Scroll Effect

Original · free

Image zooms based on scroll progress.

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

import { useRef } from "react";
import { motion, useScroll, useTransform, useReducedMotion } from "motion/react";

export default function ImgfxZoomOnScroll() {
  const reduceMotion = useReducedMotion();
  const containerRef = useRef<HTMLDivElement>(null);

  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["start end", "end start"],
  });

  const scale = useTransform(scrollYProgress, [0, 0.5, 1], reduceMotion ? [1, 1, 1] : [1.32, 1.06, 1.32]);
  const imgY = useTransform(scrollYProgress, [0, 1], reduceMotion ? ["0%", "0%"] : ["-6%", "6%"]);
  const captionY = useTransform(scrollYProgress, [0, 0.5, 1], reduceMotion ? [0, 0, 0] : [40, 0, -40]);
  const captionOpacity = useTransform(scrollYProgress, [0.15, 0.4, 0.6, 0.85], [0, 1, 1, 0]);
  const barWidth = useTransform(scrollYProgress, [0, 1], ["0%", "100%"]);

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-24 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100 sm:px-10 lg:py-32">
      <style>{`
        @keyframes zos_pulse {
          0%, 100% { opacity: 0.55; transform: scale(1); }
          50% { opacity: 1; transform: scale(1.35); }
        }
        .zos-dot { animation: zos_pulse 2.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .zos-dot { animation: none; opacity: 0.9; }
        }
      `}</style>

      <div className="relative mx-auto max-w-4xl text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
          <span className="zos-dot h-1.5 w-1.5 rounded-full bg-indigo-500 dark:bg-indigo-400" />
          Scroll-Linked Zoom
        </span>
        <h2 className="mt-6 text-balance text-3xl font-semibold tracking-tight sm:text-4xl lg:text-5xl">
          The image breathes as you scroll
        </h2>
        <p className="mx-auto mt-4 max-w-xl text-pretty text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:text-base">
          Scale is driven directly by scroll progress, no clicks, no autoplay. Keep scrolling to
          push the frame in and out.
        </p>
      </div>

      <div
        ref={containerRef}
        className="relative mx-auto mt-14 aspect-[16/10] w-full max-w-5xl overflow-hidden rounded-3xl bg-neutral-200 shadow-2xl shadow-neutral-900/10 ring-1 ring-black/5 dark:bg-neutral-900 dark:shadow-black/40 dark:ring-white/10"
      >
        <motion.div style={{ scale, y: imgY }} className="absolute inset-0 h-full w-full will-change-transform">
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g14.webp"
            alt="Mist rolling across a mountain valley at first light"
            loading="lazy"
            draggable={false}
            className="h-full w-full select-none object-cover"
          />
        </motion.div>

        {/* readability gradient */}
        <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/55 via-black/5 to-black/20" />

        {/* corner frame accents */}
        <div className="pointer-events-none absolute left-5 top-5 h-8 w-8 rounded-tl-lg border-l-2 border-t-2 border-white/70" />
        <div className="pointer-events-none absolute bottom-5 right-5 h-8 w-8 rounded-br-lg border-b-2 border-r-2 border-white/70" />

        <motion.figcaption
          style={{ y: captionY, opacity: captionOpacity }}
          className="absolute inset-x-0 bottom-0 flex flex-col gap-1 p-6 text-left will-change-transform sm:p-9"
        >
          <span className="text-[0.7rem] font-semibold uppercase tracking-[0.2em] text-white/70">
            Frame 14 / Valley Series
          </span>
          <p className="text-lg font-medium text-white sm:text-2xl">Depth follows momentum.</p>
        </motion.figcaption>

        {/* progress rail */}
        <div className="absolute inset-x-6 bottom-4 h-1 overflow-hidden rounded-full bg-white/20 sm:inset-x-9">
          <motion.div style={{ width: barWidth }} className="h-full rounded-full bg-white/90" />
        </div>
      </div>

      <p className="mx-auto mt-8 max-w-md text-center text-xs text-neutral-500 dark:text-neutral-500">
        Hint: the zoom peaks off-screen and settles as the frame centers in your viewport.
      </p>
    </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 →