Pixelate Effect
Original · freeImage pixelates then resolves on hover.
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"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
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 quoteSimilar components
Browse all →
Hover Zoom Effect
OriginalImage zooms in smoothly inside a fixed rounded frame on hover.

Hover Zoom Out Effect
OriginalImage starts zoomed and settles to normal on hover.

Zoom Rotate Effect
OriginalImage zooms and rotates slightly on hover.

Grayscale Color Effect
OriginalGreyscale image turns to full colour on hover.

Color Grayscale Effect
OriginalFull-colour image fades to greyscale on hover.

Blur Sharp Effect
OriginalBlurred image sharpens on hover.

Sharp Blur Effect
OriginalSharp image blurs with a caption appearing on hover.

Sepia Clear Effect
OriginalSepia-toned image clears to full colour on hover.

Saturate Pop Effect
OriginalMuted image saturates and pops on hover.

Contrast Pop Effect
OriginalLow-contrast image gains punchy contrast on hover.

Brightness Lift Effect
OriginalDim image brightens on hover.

Hue Rotate Effect
OriginalImage hue-shifts through colours on hover.

