Web InnoventixFreeCode

Vignette Clear Effect

Original · free

Vignetted image opens up as the vignette fades on hover.

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

import { useState } from "react";

type Shot = {
  src: string;
  alt: string;
  caption: string;
};

const shots: Shot[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Fog settling over a ridge of dark pine at first light",
    caption: "Ridgeline, dawn",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Weathered stone archway framing a quiet courtyard",
    caption: "Old quarter",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Still water mirroring a band of low evening cloud",
    caption: "Slack tide",
  },
];

export default function ImgfxVignetteClear() {
  const [active, setActive] = useState<number | null>(null);

  return (
    <section className="relative w-full bg-neutral-50 px-6 py-20 sm:py-28 dark:bg-neutral-950">
      <style>{`
        .ivc-frame {
          transition: transform 600ms cubic-bezier(0.22, 1, 0.36, 1);
        }
        .ivc-frame:hover,
        .ivc-frame:focus-within {
          transform: translateY(-4px);
        }
        .ivc-vignette {
          transition: opacity 650ms cubic-bezier(0.22, 1, 0.36, 1);
          background:
            radial-gradient(120% 120% at 50% 50%, transparent 38%, rgba(9, 9, 11, 0.72) 100%),
            linear-gradient(180deg, rgba(9, 9, 11, 0.22) 0%, transparent 26%, transparent 72%, rgba(9, 9, 11, 0.34) 100%);
        }
        .ivc-img {
          transition:
            transform 700ms cubic-bezier(0.22, 1, 0.36, 1),
            filter 650ms cubic-bezier(0.22, 1, 0.36, 1);
          filter: saturate(0.82) contrast(1.04) brightness(0.92);
        }
        .ivc-frame:hover .ivc-img,
        .ivc-frame:focus-within .ivc-img {
          transform: scale(1.045);
          filter: saturate(1.06) contrast(1) brightness(1.02);
        }
        .ivc-frame:hover .ivc-vignette,
        .ivc-frame:focus-within .ivc-vignette {
          opacity: 0;
        }
        @media (prefers-reduced-motion: reduce) {
          .ivc-frame,
          .ivc-vignette,
          .ivc-img {
            transition: none;
          }
          .ivc-frame:hover,
          .ivc-frame:focus-within {
            transform: none;
          }
          .ivc-frame:hover .ivc-img,
          .ivc-frame:focus-within .ivc-img {
            transform: none;
          }
        }
      `}</style>

      <div className="mx-auto flex max-w-6xl flex-col items-center">
        <div className="flex flex-col items-center text-center">
          <span className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Image Effect
          </span>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
            Vignette Clear
          </h2>
          <p className="mt-3 max-w-md text-sm text-neutral-500 dark:text-neutral-400">
            A darkened edge frames the photo at rest. Hover to let the vignette
            dissolve and the full image breathe open.
          </p>
        </div>

        <div className="mt-12 grid w-full grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {shots.map((shot, i) => (
            <figure
              key={shot.src}
              tabIndex={0}
              onMouseEnter={() => setActive(i)}
              onMouseLeave={() => setActive(null)}
              onFocus={() => setActive(i)}
              onBlur={() => setActive(null)}
              className="ivc-frame group relative aspect-[4/5] cursor-pointer overflow-hidden rounded-2xl bg-neutral-200 shadow-sm outline-none ring-1 ring-neutral-900/5 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10"
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={shot.src}
                alt={shot.alt}
                loading="lazy"
                draggable={false}
                className="ivc-img absolute inset-0 h-full w-full object-cover"
              />

              <div className="ivc-vignette pointer-events-none absolute inset-0" />

              <figcaption className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-between p-4">
                <span className="text-sm font-medium text-white drop-shadow-sm">
                  {shot.caption}
                </span>
                <span
                  className={`text-[10px] font-semibold uppercase tracking-[0.18em] transition-opacity duration-500 ${
                    active === i
                      ? "text-white/90 opacity-100"
                      : "text-white/70 opacity-0 group-hover:opacity-100"
                  }`}
                >
                  Clear
                </span>
              </figcaption>
            </figure>
          ))}
        </div>

        <p className="mt-8 text-xs font-medium uppercase tracking-[0.18em] text-neutral-400 dark:text-neutral-500">
          Hover or focus a photo to reveal
        </p>
      </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 →