Web InnoventixFreeCode

Pixelate Effect

Original · free

Image pixelates then resolves on hover.

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

import { useCallback, useEffect, useRef, useState } from "react";
import { useReducedMotion } from "motion/react";

const MIN_BLOCKS = 8;
const MAX_BLOCKS = 168;
const DURATION = 760;

export default function ImgfxPixelate() {
  const frameRef = useRef<HTMLDivElement>(null);
  const imgRef = useRef<HTMLImageElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const offRef = useRef<HTMLCanvasElement | null>(null);
  const rafRef = useRef<number>(0);
  const lastBlocksRef = useRef<number>(MAX_BLOCKS);
  const activeRef = useRef<boolean>(false);

  const [ready, setReady] = useState(false);
  const reduce = useReducedMotion();

  const drawAt = useCallback((blocks: number) => {
    const img = imgRef.current;
    const canvas = canvasRef.current;
    const box = frameRef.current;
    if (!img || !canvas || !box) return;
    const iw = img.naturalWidth;
    const ih = img.naturalHeight;
    if (iw === 0 || ih === 0) return;

    const w = Math.round(box.clientWidth);
    const h = Math.round(box.clientHeight);
    if (w === 0 || h === 0) return;
    if (canvas.width !== w) canvas.width = w;
    if (canvas.height !== h) canvas.height = h;

    const ctx = canvas.getContext("2d");
    if (!ctx) return;

    const bw = Math.max(2, Math.round(blocks));
    const bh = Math.max(2, Math.round(blocks * (h / w)));

    let off = offRef.current;
    if (!off) {
      off = document.createElement("canvas");
      offRef.current = off;
    }
    off.width = bw;
    off.height = bh;
    const octx = off.getContext("2d");
    if (!octx) return;

    // replicate object-fit: cover into the tiny buffer
    const scale = Math.max(bw / iw, bh / ih);
    const dw = iw * scale;
    const dh = ih * scale;
    octx.clearRect(0, 0, bw, bh);
    octx.drawImage(img, (bw - dw) / 2, (bh - dh) / 2, dw, dh);

    ctx.imageSmoothingEnabled = false;
    ctx.clearRect(0, 0, w, h);
    ctx.drawImage(off, 0, 0, bw, bh, 0, 0, w, h);
    lastBlocksRef.current = bw;
  }, []);

  const resolve = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas || !ready || reduce) return;
    activeRef.current = true;
    cancelAnimationFrame(rafRef.current);
    canvas.style.opacity = "1";
    const start = performance.now();
    const tick = (now: number) => {
      const t = Math.min(1, (now - start) / DURATION);
      const e = 1 - Math.pow(1 - t, 3);
      drawAt(MIN_BLOCKS + (MAX_BLOCKS - MIN_BLOCKS) * e);
      if (t < 1) {
        rafRef.current = requestAnimationFrame(tick);
      } else {
        canvas.style.opacity = "0";
        activeRef.current = false;
      }
    };
    rafRef.current = requestAnimationFrame(tick);
  }, [drawAt, ready, reduce]);

  const reset = useCallback(() => {
    const canvas = canvasRef.current;
    cancelAnimationFrame(rafRef.current);
    activeRef.current = false;
    if (canvas) canvas.style.opacity = "0";
  }, []);

  useEffect(() => {
    const img = imgRef.current;
    if (img && img.complete && img.naturalWidth > 0) setReady(true);
  }, []);

  useEffect(() => {
    const box = frameRef.current;
    if (!box) return;
    const ro = new ResizeObserver(() => {
      if (activeRef.current) drawAt(lastBlocksRef.current);
    });
    ro.observe(box);
    return () => {
      ro.disconnect();
      cancelAnimationFrame(rafRef.current);
    };
  }, [drawAt]);

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-6 py-20 text-slate-900 sm:py-28 dark:from-zinc-950 dark:via-zinc-900 dark:to-black dark:text-zinc-100">
      <style>{`
        @keyframes pxfx-pulse {
          0%, 100% { opacity: 0.35; transform: scale(0.85); }
          50% { opacity: 1; transform: scale(1); }
        }
        .pxfx-dot { animation: pxfx-pulse 2.4s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) {
          .pxfx-dot { animation: none; opacity: 1; }
        }
      `}</style>

      <div className="pointer-events-none absolute -top-24 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-indigo-400/20 blur-3xl dark:bg-indigo-600/20" />

      <div className="relative mx-auto flex max-w-2xl flex-col items-center gap-8">
        <div className="flex flex-col items-center gap-4 text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white/70 px-3.5 py-1.5 text-xs font-medium uppercase tracking-[0.2em] text-indigo-600 backdrop-blur dark:border-zinc-700 dark:bg-zinc-800/60 dark:text-indigo-300">
            <span className="pxfx-dot inline-block h-1.5 w-1.5 rounded-full bg-indigo-500 dark:bg-indigo-400" />
            Image FX · Pixelate
          </span>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            Pixel Resolve
          </h2>
          <p className="max-w-md text-sm leading-relaxed text-slate-600 dark:text-zinc-400">
            Hover the photo and it shatters into blocks, then sharpens back into
            focus — a clean nearest-neighbour resolve rendered on canvas.
          </p>
        </div>

        <figure
          ref={frameRef}
          tabIndex={0}
          onMouseEnter={resolve}
          onFocus={resolve}
          onMouseLeave={reset}
          onBlur={reset}
          className="group relative aspect-[4/3] w-full cursor-pointer overflow-hidden rounded-2xl bg-slate-200 shadow-xl ring-1 ring-slate-900/10 outline-none transition-shadow duration-500 focus-visible:ring-2 focus-visible:ring-indigo-500 hover:shadow-2xl dark:bg-zinc-800 dark:ring-white/10"
        >
          {/* eslint-disable-next-line @next/next/no-img-element */}
          <img
            ref={imgRef}
            src="/img/gallery/g08.webp"
            alt="Aerial landscape photograph used to demonstrate the pixel-resolve hover effect"
            loading="lazy"
            draggable={false}
            onLoad={() => setReady(true)}
            className="absolute inset-0 h-full w-full select-none object-cover"
          />
          <canvas
            ref={canvasRef}
            aria-hidden="true"
            style={{ opacity: 0, transition: "opacity 220ms ease" }}
            className="pointer-events-none absolute inset-0 h-full w-full"
          />

          <figcaption className="pointer-events-none absolute bottom-3 left-3 right-3 flex items-center justify-between text-[11px] font-medium text-white/90 [text-shadow:0_1px_3px_rgb(0_0_0/0.6)]">
            <span className="rounded-md bg-black/30 px-2 py-1 backdrop-blur-sm">
              {reduce ? "Motion reduced" : "Hover to resolve"}
            </span>
            <span className="rounded-md bg-black/30 px-2 py-1 font-mono tabular-nums backdrop-blur-sm">
              8px → sharp
            </span>
          </figcaption>
        </figure>

        <p className="text-center text-xs text-slate-500 dark:text-zinc-500">
          Real pixelation via downscale + nearest-neighbour upscale. Respects{" "}
          <code className="rounded bg-slate-200 px-1 py-0.5 font-mono text-[11px] text-slate-700 dark:bg-zinc-800 dark:text-zinc-300">
            prefers-reduced-motion
          </code>
          .
        </p>
      </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 →