Web InnoventixFreeCode

Curtain Up Effect

Original · free

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

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

import { useState } from "react";

type Curtain = {
  src: string;
  alt: string;
  title: string;
  caption: string;
};

const CURTAINS: Curtain[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit ridge line fading into layered morning haze",
    title: "First Light",
    caption: "Ridgelines, 06:12",
  },
  {
    src: "/img/gallery/g18.webp",
    alt: "Quiet city street reflecting neon after late rain",
    title: "Afterglow",
    caption: "Downtown, wet asphalt",
  },
  {
    src: "/img/gallery/g25.webp",
    alt: "Close study of textured petals in soft diffused light",
    title: "In Bloom",
    caption: "Studio macro, no flash",
  },
];

export default function ImgfxCurtainUp() {
  const [lifted, setLifted] = useState<number | null>(null);

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 sm:py-28 dark:bg-neutral-950">
      <style>{`
        @keyframes cup-shimmer {
          0% { opacity: 0.35; }
          50% { opacity: 0.7; }
          100% { opacity: 0.35; }
        }
        .cup-stage { transition: transform 720ms cubic-bezier(0.16, 1, 0.3, 1); }
        .cup-panel {
          transform: translateY(0);
          transition: transform 720ms cubic-bezier(0.7, 0, 0.2, 1);
          will-change: transform;
        }
        .cup-card:hover .cup-panel,
        .cup-card:focus-within .cup-panel {
          transform: translateY(-101%);
        }
        .cup-img {
          transform: scale(1.08);
          transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
            filter 720ms ease;
          filter: saturate(0.85);
        }
        .cup-card:hover .cup-img,
        .cup-card:focus-within .cup-img {
          transform: scale(1);
          filter: saturate(1.05);
        }
        .cup-tag {
          opacity: 0;
          transform: translateY(8px);
          transition: opacity 500ms ease 220ms, transform 500ms ease 220ms;
        }
        .cup-card:hover .cup-tag,
        .cup-card:focus-within .cup-tag {
          opacity: 1;
          transform: translateY(0);
        }
        .cup-shine { animation: cup-shimmer 3.6s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .cup-panel,
          .cup-img,
          .cup-tag,
          .cup-stage { transition: none !important; }
          .cup-card:hover .cup-panel,
          .cup-card:focus-within .cup-panel { transform: translateY(-101%); }
          .cup-img { transform: none; filter: saturate(1); }
          .cup-shine { animation: none; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <div className="mb-12 text-center">
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.35em] text-violet-600 dark:text-violet-400">
            Image Effect · Curtain Up
          </p>
          <h2 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
            A colour curtain lifts to reveal the shot
          </h2>
          <p className="mx-auto mt-4 max-w-xl text-sm text-neutral-500 dark:text-neutral-400">
            Hover or focus a frame — the panel wipes upward and the photo settles
            into place. Keyboard friendly, motion-safe.
          </p>
        </div>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {CURTAINS.map((item, i) => {
            const isLifted = lifted === i;
            return (
              <article
                key={item.src}
                tabIndex={0}
                onMouseEnter={() => setLifted(i)}
                onMouseLeave={() => setLifted(null)}
                onFocus={() => setLifted(i)}
                onBlur={() => setLifted(null)}
                aria-label={`${item.title}. ${item.caption}. Hover to reveal.`}
                className="cup-card group relative aspect-[4/5] overflow-hidden rounded-2xl border border-neutral-200 bg-neutral-900 shadow-sm outline-none ring-violet-400 transition-shadow duration-500 hover:shadow-xl focus-visible:ring-2 dark:border-neutral-800"
              >
                <div className="cup-stage absolute inset-0">
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={item.src}
                    alt={item.alt}
                    loading="lazy"
                    draggable={false}
                    className="cup-img h-full w-full object-cover"
                  />
                </div>

                <div className="pointer-events-none absolute inset-x-0 bottom-0 z-20 p-5">
                  <div className="cup-tag">
                    <p className="text-lg font-semibold text-white drop-shadow">
                      {item.title}
                    </p>
                    <p className="text-xs uppercase tracking-widest text-white/70">
                      {item.caption}
                    </p>
                  </div>
                </div>

                <div className="cup-panel absolute inset-0 z-10 flex flex-col items-center justify-center overflow-hidden bg-gradient-to-b from-indigo-600 via-violet-600 to-rose-500 dark:from-indigo-500 dark:via-violet-600 dark:to-rose-600">
                  <div className="cup-shine pointer-events-none absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_-10%,rgba(255,255,255,0.45),transparent_60%)]" />
                  <span className="relative text-sm font-medium uppercase tracking-[0.3em] text-white/90">
                    {isLifted ? "Revealing" : "Hover"}
                  </span>
                  <span className="relative mt-2 text-xs text-white/70">
                    {item.title}
                  </span>
                  <span
                    aria-hidden
                    className="relative mt-4 inline-block h-px w-10 bg-white/60"
                  />
                </div>
              </article>
            );
          })}
        </div>
      </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 →