Web InnoventixFreeCode

Shutter Effect

Original · free

A camera-shutter style reveal on hover.

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

import { useId } from "react";

export default function ImgfxShutter() {
  // Unique id so multiple instances on a page never share the same @property/animation names.
  const raw = useId();
  const uid = `imgfx-shutter-${raw.replace(/[^a-zA-Z0-9]/g, "")}`;

  const bladeCount = 6;
  const bladeStep = 360 / bladeCount;

  // Six aperture blades built from hard-stopped conic sectors with thin dividers.
  const blades = `repeating-conic-gradient(from -20deg, #0b0f16 0deg ${
    bladeStep - 2
  }deg, #1b2430 ${bladeStep - 2}deg ${bladeStep}deg)`;

  // Layered: soft center shadow for depth, metallic sheen streak, then the blades.
  const shutterBackground = [
    "radial-gradient(circle at 50% 50%, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 58%)",
    "conic-gradient(from 40deg at 50% 50%, rgba(255,255,255,0.10), rgba(255,255,255,0) 22%, rgba(255,255,255,0.06) 55%, rgba(255,255,255,0) 78%)",
    blades,
  ].join(", ");

  // A ring of transparent (the open aperture) growing out of an otherwise opaque plate.
  const aperture = `radial-gradient(circle at 50% 50%, transparent var(--${uid}-r), #000 calc(var(--${uid}-r) + 0.6%))`;

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-slate-100 px-6 py-20 sm:py-28 dark:from-neutral-950 dark:to-neutral-900">
      <style>{`
        @property --${uid}-r {
          syntax: "<percentage>";
          inherits: false;
          initial-value: 0%;
        }
        .${uid}-plate {
          --${uid}-r: 0%;
          transition:
            --${uid}-r 820ms cubic-bezier(0.16, 1, 0.3, 1),
            transform 820ms cubic-bezier(0.16, 1, 0.3, 1),
            opacity 820ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .${uid}-trigger:hover .${uid}-plate,
        .${uid}-trigger:focus-visible .${uid}-plate {
          --${uid}-r: 74%;
          transform: rotate(26deg) scale(1.7);
          opacity: 0.98;
        }
        .${uid}-ring {
          transition:
            transform 820ms cubic-bezier(0.16, 1, 0.3, 1),
            opacity 620ms ease;
        }
        .${uid}-trigger:hover .${uid}-ring,
        .${uid}-trigger:focus-visible .${uid}-ring {
          transform: scale(1.9);
          opacity: 0;
        }
        .${uid}-zoom {
          transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .${uid}-trigger:hover .${uid}-zoom,
        .${uid}-trigger:focus-visible .${uid}-zoom {
          transform: scale(1.06);
        }
        @media (prefers-reduced-motion: reduce) {
          .${uid}-plate,
          .${uid}-ring,
          .${uid}-zoom {
            transition: none;
          }
          .${uid}-trigger:hover .${uid}-plate,
          .${uid}-trigger:focus-visible .${uid}-plate {
            --${uid}-r: 74%;
            transform: rotate(26deg) scale(1.7);
          }
        }
      `}</style>

      <div className="mx-auto max-w-2xl">
        <div className="mb-8 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 text-slate-900 sm:text-4xl dark:text-slate-50">
            Camera Shutter
          </h2>
          <p className="mt-3 text-sm text-slate-500 dark:text-neutral-400">
            Hover or focus to open the aperture and let the frame through.
          </p>
        </div>

        <figure
          tabIndex={0}
          aria-label="A photograph revealed by an opening camera aperture on hover or focus."
          className={`${uid}-trigger group relative mx-auto aspect-[4/3] w-full max-w-xl cursor-pointer overflow-hidden rounded-2xl bg-slate-900 shadow-2xl shadow-slate-900/30 ring-1 ring-slate-900/10 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:shadow-black/50 dark:ring-white/10`}
        >
          {/* The photograph beneath the shutter, gently zooming as the aperture opens. */}
          <div className={`${uid}-zoom absolute inset-0`}>
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src="/img/gallery/g07.webp"
              alt="Sunlit mountain ridgeline fading into layered morning haze"
              loading="lazy"
              draggable={false}
              className="h-full w-full object-cover"
            />
          </div>

          {/* The six-blade shutter plate. Fully opaque when closed; a circular hole grows from the centre on reveal. */}
          <div
            aria-hidden="true"
            className={`${uid}-plate absolute inset-0 origin-center`}
            style={{
              background: shutterBackground,
              WebkitMaskImage: aperture,
              maskImage: aperture,
            }}
          />

          {/* A crisp aperture ring that flares outward as the blades retract. */}
          <div
            aria-hidden="true"
            className={`${uid}-ring pointer-events-none absolute left-1/2 top-1/2 h-24 w-24 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white/70 dark:border-white/60`}
          />

          <figcaption className="pointer-events-none absolute bottom-3 left-4 z-10 text-xs font-medium text-white/85 opacity-0 transition-opacity duration-500 group-hover:opacity-100 group-focus-visible:opacity-100">
            f/1.8 · 1/250s
          </figcaption>
        </figure>
      </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 →