Web InnoventixFreeCode

Zoom Blur Edge Effect

Original · free

Image zooms with soft blurred edges on hover.

byWeb InnoventixReact + Tailwind
zoombluredgeimageeffect
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-zoom-blur-edge.json
imgfx-zoom-blur-edge.tsx
"use client";

import { useState } from "react";

const PHOTOS = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit mountain ridge fading into layered morning haze",
    label: "Alpine haze",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Quiet coastal shoreline where the tide meets pale sand",
    label: "Low tide",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Dense forest canopy seen from below in soft green light",
    label: "Canopy",
  },
];

export default function ImgfxZoomBlurEdge() {
  const [active, setActive] = useState(0);
  const photo = PHOTOS[active];

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 dark:bg-neutral-950 sm:px-10 sm:py-28">
      <style>{`
        .izbe-stage {
          transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
        }
        .izbe-sharp {
          transform: scale(1.001);
          transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
            filter 900ms ease;
          -webkit-mask-image: radial-gradient(
            120% 120% at 50% 50%,
            #000 42%,
            rgba(0, 0, 0, 0.55) 64%,
            transparent 86%
          );
          mask-image: radial-gradient(
            120% 120% at 50% 50%,
            #000 42%,
            rgba(0, 0, 0, 0.55) 64%,
            transparent 86%
          );
        }
        .izbe-blur {
          transform: scale(1.12);
          filter: blur(18px) saturate(1.05);
          transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1),
            filter 900ms ease;
        }
        .izbe-card:hover .izbe-sharp {
          transform: scale(1.16);
        }
        .izbe-card:hover .izbe-blur {
          transform: scale(1.34);
          filter: blur(26px) saturate(1.12);
        }
        .izbe-card:focus-visible .izbe-sharp {
          transform: scale(1.16);
        }
        .izbe-card:focus-visible .izbe-blur {
          transform: scale(1.34);
          filter: blur(26px) saturate(1.12);
        }
        @media (prefers-reduced-motion: reduce) {
          .izbe-stage,
          .izbe-sharp,
          .izbe-blur {
            transition: none;
          }
          .izbe-card:hover .izbe-sharp,
          .izbe-card:focus-visible .izbe-sharp,
          .izbe-card:hover .izbe-blur,
          .izbe-card:focus-visible .izbe-blur {
            transform: scale(1.12);
          }
        }
      `}</style>

      <div className="mx-auto flex max-w-3xl flex-col items-center text-center">
        <span className="mb-3 inline-flex items-center gap-2 rounded-full border border-indigo-200/70 bg-indigo-50 px-3 py-1 text-xs font-medium uppercase tracking-[0.18em] text-indigo-600 dark:border-indigo-400/20 dark:bg-indigo-500/10 dark:text-indigo-300">
          Image FX
        </span>
        <h2 className="text-balance text-3xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
          Zoom with soft blurred edges
        </h2>
        <p className="mt-3 max-w-xl text-pretty text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 sm:text-base">
          The photo pushes in on hover while its frame dissolves into a gentle,
          feathered blur, keeping the eye locked on the centre.
        </p>
      </div>

      <div className="mx-auto mt-12 max-w-2xl">
        <button
          type="button"
          aria-label={`Zoom-and-blur preview: ${photo.alt}`}
          className="izbe-card group relative block w-full cursor-pointer overflow-hidden rounded-3xl bg-neutral-200 outline-none ring-offset-2 ring-offset-neutral-50 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-offset-neutral-950"
        >
          <div className="izbe-stage relative aspect-[4/3] w-full">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={photo.src}
              alt=""
              aria-hidden="true"
              loading="lazy"
              draggable={false}
              className="izbe-blur absolute inset-0 h-full w-full object-cover"
            />
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={photo.src}
              alt={photo.alt}
              loading="lazy"
              draggable={false}
              className="izbe-sharp absolute inset-0 h-full w-full object-cover"
            />
          </div>

          <div className="pointer-events-none absolute inset-0 rounded-3xl ring-1 ring-inset ring-black/5 dark:ring-white/10" />

          <div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-between bg-gradient-to-t from-black/50 to-transparent px-5 pb-4 pt-16">
            <span className="text-sm font-medium text-white drop-shadow-sm">
              {photo.label}
            </span>
            <span className="rounded-full bg-white/15 px-2.5 py-1 text-[11px] font-medium uppercase tracking-wider text-white/90 backdrop-blur-sm transition-opacity duration-300 group-hover:opacity-0">
              Hover to zoom
            </span>
          </div>
        </button>

        <div className="mt-5 flex items-center justify-center gap-2">
          {PHOTOS.map((p, i) => (
            <button
              key={p.src}
              type="button"
              onClick={() => setActive(i)}
              aria-label={`Show ${p.label}`}
              aria-pressed={i === active}
              className={`h-2.5 rounded-full transition-all duration-300 ${
                i === active
                  ? "w-7 bg-indigo-500 dark:bg-indigo-400"
                  : "w-2.5 bg-neutral-300 hover:bg-neutral-400 dark:bg-neutral-700 dark:hover:bg-neutral-600"
              }`}
            />
          ))}
        </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 →