Web InnoventixFreeCode

Curtain Scroll Effect

Original · free

A curtain wipes off the image as it scrolls in.

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

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

export default function ImgfxCurtainScroll() {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, amount: 0.45 });
  const reduceMotion = useReducedMotion();
  const reveal = reduceMotion ? true : inView;

  const ease: [number, number, number, number] = [0.76, 0, 0.24, 1];

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 text-neutral-900 sm:px-10 sm:py-28 dark:bg-neutral-950 dark:text-neutral-100">
      <style>{`
        @keyframes curtainscroll_seam {
          0%, 100% { opacity: 0.15; transform: scaleY(0.92); }
          50% { opacity: 0.5; transform: scaleY(1); }
        }
        @media (prefers-reduced-motion: reduce) {
          .curtainscroll_seam { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <div className="mb-10 max-w-xl">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.35em] text-indigo-600 dark:text-indigo-400">
            Curtain Reveal
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            The curtain parts as you scroll
          </h2>
          <p className="mt-4 text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
            Two panels sweep aside the moment the frame enters view, unveiling
            the image beneath with a clean, theatrical wipe.
          </p>
        </div>

        <div
          ref={ref}
          className="group relative aspect-[4/5] w-full overflow-hidden rounded-2xl bg-neutral-200 shadow-[0_20px_60px_-25px_rgba(0,0,0,0.5)] ring-1 ring-black/5 sm:aspect-[3/2] dark:bg-neutral-800 dark:ring-white/10"
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g14.webp"
            alt="Featured photograph unveiled by a parting curtain"
            loading="lazy"
            draggable={false}
            className="absolute inset-0 h-full w-full scale-[1.06] object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-100"
            style={{ transform: reveal ? undefined : "scale(1.12)" }}
          />

          {/* Left curtain panel */}
          <motion.div
            aria-hidden
            initial={false}
            animate={{ x: reveal ? "-101%" : "0%" }}
            transition={{ duration: reduceMotion ? 0 : 1.15, ease, delay: reduceMotion ? 0 : 0.08 }}
            className="absolute inset-y-0 left-0 z-10 w-1/2 bg-gradient-to-r from-neutral-900 to-neutral-800 dark:from-black dark:to-neutral-900"
          >
            <div className="absolute inset-y-0 right-0 w-px bg-white/20" />
          </motion.div>

          {/* Right curtain panel */}
          <motion.div
            aria-hidden
            initial={false}
            animate={{ x: reveal ? "101%" : "0%" }}
            transition={{ duration: reduceMotion ? 0 : 1.15, ease, delay: reduceMotion ? 0 : 0.08 }}
            className="absolute inset-y-0 right-0 z-10 w-1/2 bg-gradient-to-l from-neutral-900 to-neutral-800 dark:from-black dark:to-neutral-900"
          >
            <div className="absolute inset-y-0 left-0 w-px bg-white/20" />
          </motion.div>

          {/* Center seam glint that fades as the curtain opens */}
          <motion.div
            aria-hidden
            initial={false}
            animate={{ opacity: reveal ? 0 : 1 }}
            transition={{ duration: reduceMotion ? 0 : 0.5, ease }}
            className="curtainscroll_seam pointer-events-none absolute inset-y-0 left-1/2 z-20 w-[3px] -translate-x-1/2 bg-gradient-to-b from-transparent via-white/70 to-transparent"
            style={{ animation: reduceMotion ? "none" : "curtainscroll_seam 2.4s ease-in-out infinite" }}
          />

          {/* Caption chip */}
          <motion.div
            initial={false}
            animate={{ opacity: reveal ? 1 : 0, y: reveal ? 0 : 12 }}
            transition={{ duration: reduceMotion ? 0 : 0.6, ease, delay: reduceMotion ? 0 : 0.75 }}
            className="absolute bottom-4 left-4 z-30 rounded-full bg-black/55 px-4 py-1.5 text-xs font-medium tracking-wide text-white backdrop-blur-sm"
          >
            Frame No. 14
          </motion.div>
        </div>

        <p className="mt-5 text-sm text-neutral-500 dark:text-neutral-500">
          Scroll the frame into view to draw the curtain.
        </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 →