Web InnoventixFreeCode

Magnify Lens Effect

Original · free

A magnifier lens follows the cursor over the image.

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

import { useCallback, useRef, useState, type PointerEvent } from "react";

type LensState = {
  visible: boolean;
  x: number;
  y: number;
  bgX: number;
  bgY: number;
  bgW: number;
  bgH: number;
};

const IMAGE_SRC = "/img/gallery/g07.webp";
const LENS_SIZE = 168;
const ZOOM = 2.4;

const initialLens: LensState = {
  visible: false,
  x: 0,
  y: 0,
  bgX: 0,
  bgY: 0,
  bgW: 0,
  bgH: 0,
};

export default function ImgfxMagnifyLens() {
  const frameRef = useRef<HTMLDivElement>(null);
  const [lens, setLens] = useState<LensState>(initialLens);

  const updateLens = useCallback((clientX: number, clientY: number) => {
    const frame = frameRef.current;
    if (!frame) return;

    const rect = frame.getBoundingClientRect();
    const x = clientX - rect.left;
    const y = clientY - rect.top;

    if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
      setLens((prev) => (prev.visible ? { ...prev, visible: false } : prev));
      return;
    }

    const bgW = rect.width * ZOOM;
    const bgH = rect.height * ZOOM;
    const bgX = -(x * ZOOM - LENS_SIZE / 2);
    const bgY = -(y * ZOOM - LENS_SIZE / 2);

    setLens({ visible: true, x, y, bgX, bgY, bgW, bgH });
  }, []);

  const handlePointerMove = useCallback(
    (event: PointerEvent<HTMLDivElement>) => {
      updateLens(event.clientX, event.clientY);
    },
    [updateLens],
  );

  const handlePointerLeave = useCallback(() => {
    setLens((prev) => ({ ...prev, visible: false }));
  }, []);

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 sm:px-10 sm:py-28 dark:bg-neutral-950">
      <style>{`
        @keyframes imgfxMagLens_ring {
          0%, 100% { box-shadow: 0 0 0 1px rgba(255,255,255,0.9), 0 12px 40px -8px rgba(15,23,42,0.55), inset 0 0 0 2px rgba(255,255,255,0.35); }
          50% { box-shadow: 0 0 0 1px rgba(255,255,255,1), 0 12px 44px -6px rgba(15,23,42,0.6), inset 0 0 0 2px rgba(129,140,248,0.55); }
        }
        @keyframes imgfxMagLens_hint {
          0%, 100% { transform: translateX(0); opacity: 0.85; }
          50% { transform: translateX(4px); opacity: 1; }
        }
        .imgfxMagLens_lens {
          animation: imgfxMagLens_ring 2.4s ease-in-out infinite;
        }
        .imgfxMagLens_arrow {
          animation: imgfxMagLens_hint 1.8s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfxMagLens_lens,
          .imgfxMagLens_arrow {
            animation: none !important;
          }
        }
      `}</style>

      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        <span className="mb-4 inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
          Magnify Lens
        </span>
        <h2 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
          Inspect every pixel up close
        </h2>
        <p className="mt-3 max-w-xl text-base text-neutral-600 dark:text-neutral-400">
          A precision loupe that tracks your cursor and reveals fine detail in
          real time.
        </p>
      </div>

      <div className="relative mx-auto mt-12 max-w-4xl">
        <div
          ref={frameRef}
          onPointerMove={handlePointerMove}
          onPointerLeave={handlePointerLeave}
          className="group relative aspect-[16/10] w-full cursor-none overflow-hidden rounded-2xl border border-neutral-200 bg-neutral-200 shadow-xl ring-1 ring-black/5 dark:border-neutral-800 dark:bg-neutral-800 dark:ring-white/10"
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            src={IMAGE_SRC}
            alt="High-detail photograph shown at native resolution for close inspection"
            loading="lazy"
            draggable={false}
            className="h-full w-full select-none object-cover"
          />

          <div
            className="imgfxMagLens_lens pointer-events-none absolute rounded-full border border-white/70 bg-neutral-300 transition-opacity duration-150 ease-out dark:bg-neutral-700"
            style={{
              width: LENS_SIZE,
              height: LENS_SIZE,
              left: lens.x,
              top: lens.y,
              transform: "translate(-50%, -50%)",
              opacity: lens.visible ? 1 : 0,
              backgroundImage: `url(${IMAGE_SRC})`,
              backgroundRepeat: "no-repeat",
              backgroundSize: `${lens.bgW}px ${lens.bgH}px`,
              backgroundPosition: `${lens.bgX}px ${lens.bgY}px`,
            }}
          >
            <span className="absolute left-1/2 top-1/2 h-3 w-px -translate-x-1/2 -translate-y-1/2 bg-white/50" />
            <span className="absolute left-1/2 top-1/2 h-px w-3 -translate-x-1/2 -translate-y-1/2 bg-white/50" />
          </div>

          <div
            className="pointer-events-none absolute bottom-4 left-4 flex items-center gap-2 rounded-full bg-black/55 px-3 py-1.5 text-xs font-medium text-white backdrop-blur-sm transition-opacity duration-200"
            style={{ opacity: lens.visible ? 0 : 1 }}
          >
            <span className="imgfxMagLens_arrow inline-block">→</span>
            Move your cursor across the photo to magnify
          </div>
        </div>

        <p className="mt-4 text-center text-sm text-neutral-500 dark:text-neutral-500">
          {ZOOM.toFixed(1)}× optical-style loupe · follows the pointer with no lag
        </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 →