Web InnoventixFreeCode

Focus On Scroll Effect

Original · free

Image comes into focus from blur on scroll.

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

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

function FocusFrame({
  progress,
  src,
  alt,
  startBlur,
  className,
}: {
  progress: MotionValue<number>;
  src: string;
  alt: string;
  startBlur: number;
  className?: string;
}) {
  const reduce = useReducedMotion();

  const blurPx = useTransform(progress, [0, 0.85], [startBlur, 0]);
  const filter = useTransform(blurPx, (b) => `blur(${b}px) saturate(${1 + b * 0.02})`);
  const scale = useTransform(progress, [0, 0.85], [1.12, 1]);
  const glow = useTransform(progress, [0, 0.85], [0.9, 0]);

  return (
    <div className={`relative overflow-hidden rounded-2xl ${className ?? ""}`}>
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <motion.img
        src={src}
        alt={alt}
        loading="lazy"
        draggable={false}
        style={
          reduce
            ? undefined
            : {
                filter,
                scale,
                willChange: "filter, transform",
              }
        }
        className="h-full w-full object-cover"
      />
      {!reduce && (
        <motion.div
          aria-hidden="true"
          style={{ opacity: glow }}
          className="pointer-events-none absolute inset-0 bg-gradient-to-t from-indigo-500/25 via-transparent to-white/20 mix-blend-overlay"
        />
      )}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 rounded-2xl ring-1 ring-inset ring-black/10 dark:ring-white/10"
      />
    </div>
  );
}

export default function FocusOnScroll() {
  const sectionRef = useRef<HTMLElement>(null);
  const reduce = useReducedMotion();

  const { scrollYProgress } = useScroll({
    target: sectionRef,
    offset: ["start 0.9", "center 0.55"],
  });

  return (
    <section
      ref={sectionRef}
      className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-6 py-24 text-slate-900 sm:px-10 lg:py-32 dark:from-zinc-950 dark:to-black dark:text-zinc-100"
    >
      <style>{`
        @keyframes fxfocus-rise {
          from { opacity: 0; transform: translateY(14px); }
          to   { opacity: 1; transform: translateY(0); }
        }
        .fxfocus-rise {
          animation: fxfocus-rise 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
        }
        .fxfocus-rise-2 { animation-delay: 0.08s; }
        @media (prefers-reduced-motion: reduce) {
          .fxfocus-rise, .fxfocus-rise-2 { animation: none; }
        }
      `}</style>

      {/* soft backdrop accents */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -left-24 top-10 h-72 w-72 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-500/10"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute -right-24 bottom-10 h-72 w-72 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/10"
      />

      <div className="relative mx-auto max-w-5xl">
        <header className={`mb-12 max-w-2xl ${reduce ? "" : "fxfocus-rise"}`}>
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Focus on scroll
          </p>
          <h2 className="text-balance text-3xl font-semibold tracking-tight sm:text-4xl lg:text-5xl">
            It sharpens as you arrive.
          </h2>
          <p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-zinc-400">
            The photograph starts soft and out of focus, then resolves to crisp
            detail as it settles into the viewport.
          </p>
        </header>

        <div className={`grid grid-cols-1 gap-4 sm:grid-cols-3 ${reduce ? "" : "fxfocus-rise fxfocus-rise-2"}`}>
          <FocusFrame
            progress={scrollYProgress}
            src="/img/gallery/g07.webp"
            alt="Portrait of a coastal cliff catching first light"
            startBlur={20}
            className="aspect-[4/5] sm:col-span-2 sm:row-span-2"
          />
          <FocusFrame
            progress={scrollYProgress}
            src="/img/gallery/g14.webp"
            alt="Close detail of textured stone in warm shade"
            startBlur={14}
            className="aspect-[4/3]"
          />
          <FocusFrame
            progress={scrollYProgress}
            src="/img/gallery/g22.webp"
            alt="Quiet harbour at dusk with still water"
            startBlur={14}
            className="aspect-[4/3]"
          />
        </div>

        <p className="mt-6 text-sm text-slate-500 dark:text-zinc-500">
          Scroll the section into view to bring the frames into focus.
        </p>
      </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 →