Web InnoventixFreeCode

Parallax Effect

Original · free

Image parallaxes within its frame as you scroll.

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

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

export default function ImgfxParallax() {
  const reduce = useReducedMotion();

  const frameRef = useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: frameRef,
    offset: ["start end", "end start"],
  });

  // Image drifts vertically within its clipped frame as the frame crosses the viewport.
  const imageY = useTransform(
    scrollYProgress,
    [0, 1],
    reduce ? ["0%", "0%"] : ["-14%", "14%"],
  );
  // A subtler counter-drift on the caption for depth separation.
  const captionY = useTransform(
    scrollYProgress,
    [0, 1],
    reduce ? ["0%", "0%"] : ["8%", "-8%"],
  );
  const sheenX = useTransform(
    scrollYProgress,
    [0, 1],
    reduce ? ["0%", "0%"] : ["-40%", "140%"],
  );

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-24 text-neutral-900 sm:px-10 lg:py-32 dark:bg-neutral-950 dark:text-neutral-100">
      <style>{`
        @keyframes imgfxpx_float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-6px); }
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfxpx_float { animation: none !important; }
        }
      `}</style>

      {/* atmosphere */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-0 opacity-70 [background:radial-gradient(60%_50%_at_50%_0%,theme(colors.indigo.200/.45),transparent_70%)] dark:opacity-40 dark:[background:radial-gradient(60%_50%_at_50%_0%,theme(colors.indigo.500/.28),transparent_70%)]"
      />

      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-neutral-300/70 bg-white/60 px-3.5 py-1.5 text-[11px] font-semibold uppercase tracking-[0.22em] text-indigo-700 backdrop-blur dark:border-white/10 dark:bg-white/5 dark:text-indigo-300">
          <span className="imgfxpx_float inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
          Parallax Frame
        </span>

        <h2 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
          The image drifts inside its window
        </h2>
        <p className="mt-4 max-w-xl text-pretty text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
          Scroll and the photograph glides against a fixed crop, so the frame
          holds still while the world behind it moves.
        </p>
      </div>

      <div className="relative mx-auto mt-16 max-w-4xl">
        {/* the fixed frame — image is oversized and translated within */}
        <motion.div
          ref={frameRef}
          className="group relative aspect-[16/10] overflow-hidden rounded-3xl border border-neutral-900/10 bg-neutral-200 shadow-[0_30px_80px_-30px_rgba(30,27,75,0.45)] ring-1 ring-white/40 dark:border-white/10 dark:bg-neutral-800 dark:shadow-[0_30px_90px_-30px_rgba(0,0,0,0.8)] dark:ring-white/5"
        >
          <motion.div style={{ y: imageY }} className="absolute -inset-y-[20%] inset-x-0">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src="/img/gallery/g14.webp"
              alt="Mountain ridge fading into layered morning haze"
              loading="lazy"
              draggable={false}
              className="h-full w-full object-cover"
            />
          </motion.div>

          {/* legibility + depth veil */}
          <div
            aria-hidden
            className="pointer-events-none absolute inset-0 bg-gradient-to-t from-neutral-950/55 via-neutral-950/5 to-transparent"
          />

          {/* moving sheen tied to scroll */}
          <motion.div
            aria-hidden
            style={{ x: sheenX }}
            className="pointer-events-none absolute inset-y-0 -left-1/2 w-1/2 skew-x-[-12deg] bg-gradient-to-r from-transparent via-white/25 to-transparent"
          />

          {/* inner edge sharpening */}
          <div
            aria-hidden
            className="pointer-events-none absolute inset-0 rounded-3xl ring-1 ring-inset ring-white/15"
          />

          {/* caption rides a gentle counter-parallax */}
          <motion.div
            style={{ y: captionY }}
            className="absolute inset-x-5 bottom-5 flex items-end justify-between gap-4 sm:inset-x-7 sm:bottom-7"
          >
            <div className="text-left">
              <p className="text-xs font-medium uppercase tracking-[0.2em] text-white/70">
                Frame 14 / 36
              </p>
              <p className="mt-1 text-lg font-semibold text-white drop-shadow-sm sm:text-xl">
                Ridgeline at first light
              </p>
            </div>
            <span className="hidden shrink-0 rounded-full border border-white/25 bg-white/10 px-3 py-1 text-xs font-medium text-white/85 backdrop-blur sm:inline-block">
              depth · scroll
            </span>
          </motion.div>
        </motion.div>

        <p className="mt-5 text-center text-sm text-neutral-500 dark:text-neutral-500">
          Keep scrolling to feel the vertical drift — the crop never moves.
        </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 →