Web InnoventixFreeCode

Grain Effect

Original · free

A film-grain overlay that fades on hover.

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

import { useReducedMotion } from "motion/react";

const GRAIN_SVG =
  "data:image/svg+xml;utf8," +
  encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'>` +
      `<filter id='n'>` +
      `<feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/>` +
      `<feColorMatrix type='saturate' values='0'/>` +
      `</filter>` +
      `<rect width='100%' height='100%' filter='url(#n)'/>` +
      `</svg>`,
  );

export default function ImgfxGrain() {
  const reduceMotion = useReducedMotion();

  return (
    <section className="relative w-full bg-neutral-50 px-6 py-20 sm:py-28 dark:bg-neutral-950">
      <style>{`
        @keyframes imgfxgrain-drift {
          0%   { transform: translate(0, 0); }
          20%  { transform: translate(-6%, 4%); }
          40%  { transform: translate(3%, -7%); }
          60%  { transform: translate(-4%, -3%); }
          80%  { transform: translate(5%, 5%); }
          100% { transform: translate(0, 0); }
        }
        .imgfxgrain-noise {
          animation: imgfxgrain-drift 0.6s steps(4) infinite;
        }
        .imgfxgrain-frame:hover .imgfxgrain-veil {
          opacity: 0;
        }
        .imgfxgrain-frame:hover .imgfxgrain-vignette {
          opacity: 0.15;
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfxgrain-noise { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-4xl">
        <div className="mb-8 text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-rose-600 dark:text-rose-400">
            Image Effect / Film Grain
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
            Analog grain, digital clarity
          </h2>
          <p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">
            A photochemical grain layer sits over the frame. Hover to let it
            dissolve and reveal the clean image beneath.
          </p>
        </div>

        <figure
          className="imgfxgrain-frame group relative mx-auto aspect-[4/5] w-full max-w-xl overflow-hidden rounded-2xl bg-neutral-200 shadow-2xl ring-1 ring-neutral-900/10 sm:aspect-[3/4] dark:bg-neutral-800 dark:ring-white/10"
          tabIndex={0}
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src="/img/gallery/g14.webp"
            alt="Portrait photograph rendered with a soft film-grain texture overlay"
            loading="lazy"
            draggable={false}
            className="h-full w-full object-cover"
          />

          {/* vignette */}
          <div
            className="imgfxgrain-vignette pointer-events-none absolute inset-0 opacity-40 transition-opacity duration-700 ease-out"
            style={{
              background:
                "radial-gradient(120% 90% at 50% 45%, transparent 55%, rgba(0,0,0,0.55) 100%)",
            }}
          />

          {/* grain veil */}
          <div className="imgfxgrain-veil pointer-events-none absolute inset-0 opacity-100 mix-blend-overlay transition-opacity duration-700 ease-out">
            <div
              className={reduceMotion ? "" : "imgfxgrain-noise"}
              style={{
                position: "absolute",
                inset: "-30%",
                backgroundImage: `url("${GRAIN_SVG}")`,
                backgroundSize: "140px 140px",
                opacity: 0.55,
              }}
            />
          </div>

          <figcaption className="pointer-events-none absolute bottom-0 left-0 right-0 flex items-center justify-between gap-2 bg-gradient-to-t from-black/60 to-transparent px-4 pb-4 pt-10">
            <span className="text-sm font-medium text-white/90">
              Kodak 400 — pushed one stop
            </span>
            <span className="rounded-full bg-white/15 px-2.5 py-1 text-[11px] font-medium text-white/80 backdrop-blur-sm transition-opacity duration-500 group-hover:opacity-0">
              hover to clear
            </span>
          </figcaption>
        </figure>
      </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 →