Web InnoventixFreeCode

Mask Wipe Scroll Effect

Original · free

A mask wipes to reveal the image driven by scroll.

byWeb InnoventixReact + Tailwind
maskwipescrollimageeffect
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-mask-wipe-scroll.json
imgfx-mask-wipe-scroll.tsx
"use client";

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

function useWipe(progress: MotionValue<number>, reduce: boolean) {
  const smooth = useSpring(progress, {
    stiffness: 120,
    damping: 28,
    mass: 0.6,
  });
  const source = reduce ? progress : smooth;

  // Diagonal wipe: the covering panel slides fully off as we scroll through.
  const clip = useTransform(source, [0, 1], [0, 100]);
  const panelX = useTransform(clip, (v) => `${-v}%`);
  const imageScale = useTransform(source, [0, 1], [1.18, 1]);
  const imageX = useTransform(source, [0, 1], [24, 0]);

  return { panelX, imageScale, imageX };
}

export default function ImgfxMaskWipeScroll() {
  const reduce = useReducedMotion() ?? false;
  const ref = useRef<HTMLDivElement>(null);

  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start 0.85", "center 0.4"],
  });

  const { panelX, imageScale, imageX } = useWipe(scrollYProgress, reduce);

  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 mws_sheen {
          0% { transform: translateX(-120%) skewX(-12deg); opacity: 0; }
          40% { opacity: 0.9; }
          100% { transform: translateX(220%) skewX(-12deg); opacity: 0; }
        }
        @media (prefers-reduced-motion: reduce) {
          .mws_sheen { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto flex max-w-5xl flex-col items-start gap-10">
        <header className="max-w-2xl">
          <p className="mb-4 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.25em] text-indigo-600 dark:text-indigo-400">
            <span className="h-px w-8 bg-indigo-500/70" aria-hidden="true" />
            Scroll Mask Wipe
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            The picture uncovers itself as you scroll
          </h2>
          <p className="mt-4 text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
            A single panel slides away on scroll, wiping the frame clean to
            reveal the photo beneath, no click, no hover, just motion tied to
            the page.
          </p>
        </header>

        <div
          ref={ref}
          className="relative w-full overflow-hidden rounded-3xl border border-neutral-200 bg-neutral-100 shadow-[0_30px_80px_-40px_rgba(0,0,0,0.5)] ring-1 ring-black/5 dark:border-neutral-800 dark:bg-neutral-900 dark:ring-white/5"
        >
          <div className="relative aspect-[16/10] w-full">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <motion.img
              src="/img/gallery/g14.webp"
              alt="A wide landscape photograph gradually revealed by a scroll-driven wipe"
              loading="lazy"
              draggable={false}
              style={{ scale: imageScale, x: imageX }}
              className="absolute inset-0 h-full w-full object-cover will-change-transform"
            />

            {/* subtle gradient for text legibility */}
            <div
              className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/45 via-transparent to-transparent"
              aria-hidden="true"
            />

            {/* the wiping panel */}
            <motion.div
              style={{ x: panelX }}
              className="pointer-events-none absolute inset-0 overflow-hidden bg-neutral-200 dark:bg-neutral-800 will-change-transform"
              aria-hidden="true"
            >
              <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/15 via-transparent to-violet-500/15" />
              <div className="absolute right-0 top-0 h-full w-24 bg-gradient-to-l from-black/20 to-transparent dark:from-black/40" />
              <span
                className="mws_sheen absolute inset-y-0 right-0 w-32 bg-white/50 blur-md"
                style={{ animation: "mws_sheen 2.6s ease-in-out infinite" }}
              />
            </motion.div>

            {/* leading edge accent line rides with the panel */}
            <motion.div
              style={{ x: panelX }}
              className="pointer-events-none absolute inset-y-0 right-0 w-[3px] bg-indigo-500 shadow-[0_0_20px_2px_rgba(99,102,241,0.6)] will-change-transform"
              aria-hidden="true"
            />

            <p className="absolute bottom-4 left-5 z-10 text-sm font-medium text-white/90 drop-shadow">
              g14 — revealed on scroll
            </p>
          </div>
        </div>

        <p className="text-sm text-neutral-500 dark:text-neutral-500">
          Hint: scroll this block into the middle of the viewport to sweep the
          panel away.
        </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 →