Web InnoventixFreeCode

Curtain Down Effect

Original · free

A colour curtain wipes down to reveal the image on hover.

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

import { useId } from "react";

type CurtainPhoto = {
  src: string;
  alt: string;
  tint: string;
};

const PHOTOS: CurtainPhoto[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Morning light spilling across a quiet studio workspace",
    tint: "from-indigo-500 via-violet-500 to-indigo-700",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Textured cityscape fading into low evening haze",
    tint: "from-rose-500 via-amber-500 to-rose-600",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Still life of layered natural forms in soft shadow",
    tint: "from-emerald-500 via-teal-500 to-emerald-700",
  },
];

export default function ImgfxCurtainDown() {
  const uid = useId().replace(/[:]/g, "");
  const prefix = `curtdn-${uid}`;

  return (
    <section className="relative w-full overflow-hidden bg-white px-6 py-20 text-slate-900 sm:px-10 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        .${prefix}-panel {
          transition: transform 720ms cubic-bezier(0.7, 0, 0.2, 1);
          will-change: transform;
        }
        .${prefix}-group:hover .${prefix}-panel,
        .${prefix}-group:focus-visible .${prefix}-panel {
          transform: translateY(100%);
        }
        .${prefix}-seam {
          transition: opacity 320ms ease, transform 720ms cubic-bezier(0.7, 0, 0.2, 1);
        }
        .${prefix}-group:hover .${prefix}-seam,
        .${prefix}-group:focus-visible .${prefix}-seam {
          transform: translateY(100%);
          opacity: 0;
        }
        .${prefix}-img {
          transform: scale(1.06);
          transition: transform 900ms cubic-bezier(0.2, 0.7, 0.2, 1);
        }
        .${prefix}-group:hover .${prefix}-img,
        .${prefix}-group:focus-visible .${prefix}-img {
          transform: scale(1);
        }
        @media (prefers-reduced-motion: reduce) {
          .${prefix}-panel,
          .${prefix}-seam,
          .${prefix}-img {
            transition-duration: 1ms;
          }
          .${prefix}-img { transform: none; }
        }
      `}</style>

      <div className="mx-auto max-w-5xl">
        <div className="mx-auto max-w-2xl text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.28em] text-indigo-600 dark:text-indigo-400">
            Image Effect
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">
            Curtain Down
          </h2>
          <p className="mt-3 text-sm text-slate-600 dark:text-slate-400">
            A colour curtain drops away to reveal the photograph.{" "}
            <span className="font-medium text-slate-800 dark:text-slate-200">
              Hover or focus a frame.
            </span>
          </p>
        </div>

        <ul className="mt-14 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {PHOTOS.map((photo, i) => (
            <li key={photo.src}>
              <figure
                tabIndex={0}
                aria-label={photo.alt}
                className={`${prefix}-group group relative block aspect-[4/5] w-full overflow-hidden rounded-2xl bg-slate-200 shadow-sm ring-1 ring-slate-900/5 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-slate-800 dark:ring-white/10`}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={photo.src}
                  alt={photo.alt}
                  loading="lazy"
                  draggable={false}
                  className={`${prefix}-img absolute inset-0 h-full w-full object-cover`}
                />

                {/* colour curtain */}
                <div
                  aria-hidden="true"
                  className={`${prefix}-panel absolute inset-0 z-10 bg-gradient-to-b ${photo.tint}`}
                >
                  <span className="absolute inset-0 opacity-30 mix-blend-overlay [background:radial-gradient(120%_80%_at_50%_0%,rgba(255,255,255,0.55),transparent_60%)]" />
                  <span className="absolute bottom-6 left-1/2 -translate-x-1/2 text-[0.65rem] font-semibold uppercase tracking-[0.3em] text-white/85">
                    0{i + 1}
                  </span>
                </div>

                {/* trailing seam that rides the curtain edge */}
                <div
                  aria-hidden="true"
                  className={`${prefix}-seam absolute inset-x-0 top-0 z-20 h-px bg-white/70 shadow-[0_6px_20px_rgba(0,0,0,0.25)]`}
                />
              </figure>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 →