Pixelated Image Effect
A cursor or hover-based image effect that pixelates, reveals, or distorts visuals for digital, retro, gaming, and experimental interfaces.
Overview
Pixelated Image turns pointer movement into atmosphere: images break into pixelated texture.
Use it on creative homepages, portfolios, campaign pages, and brand experiments where the cursor can carry the visual language. Keep it off dashboards, checkout, forms, and utility-heavy product screens.
In production, the risk is restraint. Treat the cursor layer as throwaway: switch it off on touch, keep focus states visible, and let nothing about clicking depend on it. If the page falls apart once you remove the effect, the cursor was doing work it should not own.
Install Command
npx hyperiux add pixelated-image-effectUsage Code
import PixelatedImageEffect from "@/components/effects/pixelated-image-effect";
export default function Page() {
return (
<>
<PixelatedImageEffect
src="https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/v-02.jpg"
alt="Pixelated nature scene"
/>
</>
);
}
Component Code
// Built using Hyperiux Vault: https://vault.hyperiux.com
"use client";
import { useEffect, useId, useRef, useState } from "react";
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setPrefersReducedMotion(mediaQuery.matches);
update();
mediaQuery.addEventListener("change", update);
return () => mediaQuery.removeEventListener("change", update);
}, []);
return prefersReducedMotion;
}
export function PixelateSvgFilter({ id: idProp, size: propSize = 16, crossLayers = false }) {
const generatedId = `pixelate-filter-${useId().replace(/:/g, "")}`;
const id = idProp ?? generatedId;
const size = Math.max(2, propSize);
return (<svg aria-hidden="true" className="pointer-events-none absolute h-0 w-0 overflow-hidden">
<defs>
<filter id={id} x="0" y="0" width="1" height="1">
{/* Base pixelation */}
<feConvolveMatrix kernelMatrix="1 1 1
1 1 1
1 1 1" result="AVG"/>
<feFlood x="1" y="1" width="1" height="1"/>
<feComposite operator="arithmetic" k1="0" k2="1" k3="0" k4="0" width={size} height={size}/>
<feTile result="TILE"/>
<feComposite in="AVG" in2="TILE" operator="in"/>
<feMorphology operator="dilate" radius={size / 2} result="NORMAL"/>
{crossLayers && (<>
{/* Horizontal fallback */}
<feConvolveMatrix kernelMatrix="1 1 1
1 1 1
1 1 1" result="AVG"/>
<feFlood x="1" y="1" width="1" height="1"/>
<feComposite in2="SourceGraphic" operator="arithmetic" k1="0" k2="1" k3="0" k4="0" width={size / 2} height={size}/>
<feTile result="TILE"/>
<feComposite in="AVG" in2="TILE" operator="in"/>
<feMorphology operator="dilate" radius={size / 2} result="FALLBACKX"/>
{/* Vertical fallback */}
<feConvolveMatrix kernelMatrix="1 1 1
1 1 1
1 1 1" result="AVG"/>
<feFlood x="1" y="1" width="1" height="1"/>
<feComposite in2="SourceGraphic" operator="arithmetic" k1="0" k2="1" k3="0" k4="0" width={size} height={size / 2}/>
<feTile result="TILE"/>
<feComposite in="AVG" in2="TILE" operator="in"/>
<feMorphology operator="dilate" radius={size / 2} result="FALLBACKY"/>
<feMerge>
<feMergeNode in="FALLBACKX"/>
<feMergeNode in="FALLBACKY"/>
<feMergeNode in="NORMAL"/>
</feMerge>
</>)}
{!crossLayers && <feMergeNode in="NORMAL"/>}
</filter>
</defs>
</svg>);
}
export default function PixelatedImageEffect({ src = "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/v-02.jpg", pixelSize = 16, duration = 0.25, mouseReactivity = 1, imageClassName = "", }) {
const imageRef = useRef(null);
const isTouching = useRef(false);
const animationRef = useRef(null);
const [renderedPixelSize, setRenderedPixelSize] = useState(pixelSize);
const [targetPixelSize, setTargetPixelSize] = useState(pixelSize);
const pixelateFilterId = `pixelate-filter-${useId().replace(/:/g, "")}`;
const prefersReducedMotion = usePrefersReducedMotion();
const fallbackSrc = "https://pub-8abee449136941f5b0a1cd2c014534e9.r2.dev/vault-listing-images/assets-images/v-02.jpg";
const resolvedSrc = typeof src === "string" && src.trim() ? src.trim() : fallbackSrc;
const resolvedPixelSize = Math.min(Math.max(Number(pixelSize) || 16, 1), 64);
const resolvedDuration = Math.max(Number(duration) || 0, 0);
const resolvedMouseReactivity = Math.min(Math.max(Number(mouseReactivity) || 0, 0), 2);
useEffect(() => {
setTargetPixelSize(resolvedPixelSize);
}, [resolvedPixelSize]);
useEffect(() => {
if (animationRef.current !== null)
cancelAnimationFrame(animationRef.current);
if (resolvedDuration === 0) {
setRenderedPixelSize(targetPixelSize);
return undefined;
}
const startValue = renderedPixelSize;
const distance = targetPixelSize - startValue;
const startTime = performance.now();
const durationMs = resolvedDuration * 1000;
const animate = (time) => {
const progress = Math.min((time - startTime) / durationMs, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setRenderedPixelSize(startValue + distance * eased);
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate);
}
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current !== null)
cancelAnimationFrame(animationRef.current);
};
}, [resolvedDuration, targetPixelSize]);
const updatePixel = (event) => {
if (!imageRef.current)
return;
const rect = imageRef.current.getBoundingClientRect();
const x = event.clientX - rect.left;
const pointerPixelSize = Math.min(Math.max(x / 30, 1), 64);
const nextPixelSize = resolvedPixelSize + (pointerPixelSize - resolvedPixelSize) * resolvedMouseReactivity;
setTargetPixelSize(Math.min(Math.max(nextPixelSize, 1), 64));
};
const handlePointerDown = (event) => {
isTouching.current = true;
updatePixel(event);
};
const handlePointerMove = (event) => {
if (event.pointerType === "touch" && !isTouching.current)
return;
updatePixel(event);
};
const handlePointerUp = () => {
isTouching.current = false;
};
return (<div className="relative flex h-dvh w-dvw pt-6 flex-col gap-15 items-center justify-center">
<h1 className="text-5xl text-center max-[1025px]:hidden w-[45%] mt-10">
Move cursor left in the image block for a clear image, right for more pixelation.
</h1>
<h2 className="hidden max-[1025px]:block w-[60%] text-center mx-auto text-2xl">
Click left in image block for clear image, right for more pixelated
</h2>
<PixelateSvgFilter id={pixelateFilterId} size={renderedPixelSize} crossLayers/>
<div ref={imageRef} className="relative h-[55vh] w-full max-md:max-w-[90%] overflow-hidden max-[1025px]:max-w-[90%] max-w-lg touch-none" style={{ filter: `url(#${pixelateFilterId})` }} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerLeave={handlePointerUp}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={resolvedSrc} alt="" className={`object-cover absolute inset-0 ${imageClassName}`.trim()}/>
</div>
{prefersReducedMotion && (<div aria-live="polite" className="pointer-events-none fixed bottom-4 right-4 z-40 w-fit max-w-65 rounded-md border border-white/15 bg-white/5 p-3 text-center backdrop-blur-sm max-[1025px]:hidden">
<h2 className="text-sm leading-none text-white">
The pixels keep shifting.
</h2>
<p className="mt-2 text-xs leading-5 text-white/65">
Pixelated Image Effect changes pixelation based on cursor
position in real time. Since the transition is driven entirely by
motion, reduced motion can't be applied here.
</p>
</div>)}
</div>);
}
Example Production Use Case
Use this as cursor-system implementation guidance. Verify the shipped component export, pointer-tracking model, coarse-pointer disablement, z-index behavior, focus safety, cleanup, and reduced-motion handling before relying on exact props, defaults, imports, or installation steps.
Best Used For
- Digital, gaming, and retro-technical pages where pixel texture belongs to the visual system.
- Portfolio previews where pixelation adds character without obscuring the final image.
- Pixelated Image gives pointer movement a brand role without hiding core controls or content.
Not For
Not for dashboards, checkout, forms, dense product UIs, or any flow where precision beats atmosphere.
Performance Budget
Use one cursor layer, throttle movement with requestAnimationFrame, avoid full-screen filter effects, and disable Pixelated Image on coarse pointers.
Accessibility and Mobile
Switch it off for coarse pointers and leave touch native; anything revealed by the cursor needs a visible or tap-accessible alternative.
Common Mistakes
- Letting Pixelated Image cover focus rings or clickable controls.
- Forgetting to disable the effect on coarse pointers.
- Hiding useful content behind mouse-only movement.
Changelog
v1.1.0
Jul 22, 2026v1.0.0
Jun 4, 2026Props
| Prop | Type | Default | Description |
|---|---|---|---|
pixelSize | number | 16 | Baseline pixel size for the SVG pixelation filter. |
duration | number | 0.25 | Smoothing duration for pixel-size changes. |
mouseReactivity | number | 1 | Strength of cursor-driven pixelation changes. |
Frequently Asked Questions
When should I use Pixelated Image?
Use it when a technical or editorial page needs images breaking into pixelated texture on hover — not on utility flows.
Does Pixelated Image work on mobile?
Disable it on coarse pointers with CSS and JS pointer queries, then provide native touch behavior.
Can Pixelated Image reveal important content?
Only when the same content is visible or accessible without pointer movement. Cursor-only reveals are not suitable for critical information.
How should Pixelated Image behave around links and buttons?
Native click targets, visible focus, hover states, and modal stacking all have to keep working underneath it.
What should reduced motion do for Pixelated Image?
Disable trails, lag, ripples, and pointer-following motion, then return to the native cursor.
Request a Custom Cursor Animation
Need a custom effect? Tell us what to create.

