Overlays
Pixelated Image Trail
#cursor#trail#pixel
Move your cursor
Pixelated Image Trail
Hover across the preview to spawn a sliced image trail with staggered reveal.
implementedInfinityUI component
This component is fully implemented in InfinityUI and wired into the docs and registry flow.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/pixelated-image-trailPackage Dependencies
Install the npm packages used by this component source.
npm install clsx tailwind-mergeComponent Code
Copy and paste this code into your component file.
tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { cn } from "@/lib/utils";
type TrailConfig = {
imageLifespan: number;
inDuration: number;
outDuration: number;
staggerIn: number;
staggerOut: number;
slideDuration: number;
slideEasing: string;
easing: string;
};
const defaultTrailConfig: TrailConfig = {
imageLifespan: 400,
inDuration: 150,
outDuration: 300,
staggerIn: 6,
staggerOut: 4,
slideDuration: 900,
slideEasing: "cubic-bezier(0.16, 1, 0.3, 1)",
easing: "cubic-bezier(0.16, 1, 0.3, 1)",
};
const defaultImages = Array.from({ length: 3 }, (_, index) => {
const palette = [
["#4f46e5", "#0f172a"],
["#0891b2", "#111827"],
["#db2777", "#111827"],
][index];
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320" viewBox="0 0 320 320">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${palette[0]}" />
<stop offset="100%" stop-color="${palette[1]}" />
</linearGradient>
</defs>
<rect width="320" height="320" rx="28" fill="url(#bg)" />
<rect x="20" y="20" width="280" height="280" rx="22" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.12)" />
<text x="36" y="238" fill="white" font-family="Inter, Arial, sans-serif" font-size="42" font-weight="700">Trail ${index + 1}</text>
<text x="36" y="272" fill="rgba(255,255,255,0.72)" font-family="Inter, Arial, sans-serif" font-size="18" font-weight="500">Pixelated Image Trail</text>
</svg>
`;
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(
svg.replace(/\s+/g, " ").trim()
)}`;
});
export interface PixelatedImageTrailProps {
className?: string;
images?: string[];
config?: Partial<TrailConfig>;
slices?: number;
spawnThreshold?: number;
smoothing?: number;
usePortal?: boolean;
}
export function PixelatedImageTrail({
className,
images = [],
config: configOverride = {},
slices = 4,
spawnThreshold = 100,
smoothing = 0.1,
usePortal = true,
}: PixelatedImageTrailProps) {
const [mounted, setMounted] = useState(false);
const trailContainerRef = useRef<HTMLDivElement>(null);
const currentImageIndexRef = useRef(0);
const mousePosRef = useRef({ x: 0, y: 0 });
const lastMousePosRef = useRef({ x: 0, y: 0 });
const interpolatedMousePosRef = useRef({ x: 0, y: 0 });
const animationFrameRef = useRef<number | null>(null);
const validImagesRef = useRef<string[]>([]);
const finalImages = images.length > 0 ? images : defaultImages;
useEffect(() => {
validImagesRef.current = [];
finalImages.forEach((src) => {
const image = new Image();
image.src = src;
image.onload = () => {
validImagesRef.current.push(src);
};
});
}, [finalImages]);
useEffect(() => {
queueMicrotask(() => setMounted(true));
}, []);
useEffect(() => {
if (!mounted) {
return;
}
const config = { ...defaultTrailConfig, ...configOverride };
const trailContainer = trailContainerRef.current;
if (!trailContainer) {
return;
}
const mathUtils = {
lerp: (from: number, to: number, amount: number) =>
(1 - amount) * from + amount * to,
distance: (x1: number, y1: number, x2: number, y2: number) =>
Math.hypot(x2 - x1, y2 - y1),
};
const getMouseDistance = () =>
mathUtils.distance(
interpolatedMousePosRef.current.x,
interpolatedMousePosRef.current.y,
lastMousePosRef.current.x,
lastMousePosRef.current.y
);
const createTrailImage = () => {
if (validImagesRef.current.length === 0) {
return;
}
const imageContainer = document.createElement("div");
imageContainer.classList.add("pixelated-trail-img");
const imageSrc =
validImagesRef.current[
currentImageIndexRef.current % validImagesRef.current.length
];
currentImageIndexRef.current =
(currentImageIndexRef.current + 1) % validImagesRef.current.length;
const rect = trailContainer.getBoundingClientRect();
const startX = interpolatedMousePosRef.current.x - rect.left - 87.5;
const startY = interpolatedMousePosRef.current.y - rect.top - 87.5;
const dx = mousePosRef.current.x - interpolatedMousePosRef.current.x;
const dy = mousePosRef.current.y - interpolatedMousePosRef.current.y;
const targetX = startX + dx * 0.5;
const targetY = startY + dy * 0.5;
imageContainer.style.transform = "translate3d(0, 0, 0)";
imageContainer.style.left = `${startX}px`;
imageContainer.style.top = `${startY}px`;
imageContainer.style.transition = `left ${config.slideDuration}ms ${config.slideEasing}, top ${config.slideDuration}ms ${config.slideEasing}`;
const maskLayers: HTMLDivElement[] = [];
for (let index = 0; index < slices; index += 1) {
const layer = document.createElement("div");
layer.classList.add("pixelated-mask-layer");
const imageLayer = document.createElement("div");
imageLayer.classList.add("pixelated-image-layer");
imageLayer.style.backgroundImage = `url(${imageSrc})`;
const sliceSize = 100 / slices;
const startClipY = index * sliceSize;
const endClipY = (index + 1) * sliceSize;
layer.style.clipPath = `polygon(50% ${startClipY}%, 50% ${startClipY}%, 50% ${endClipY}%, 50% ${endClipY}%)`;
layer.style.transition = `clip-path ${config.inDuration}ms ${config.easing}`;
layer.style.transform = "translateZ(0)";
layer.style.backfaceVisibility = "hidden";
layer.appendChild(imageLayer);
imageContainer.appendChild(layer);
maskLayers.push(layer);
}
trailContainer.appendChild(imageContainer);
requestAnimationFrame(() => {
imageContainer.style.left = `${targetX}px`;
imageContainer.style.top = `${targetY}px`;
maskLayers.forEach((layer, index) => {
const sliceSize = 100 / slices;
const startClipY = index * sliceSize;
const endClipY = (index + 1) * sliceSize;
const distanceFromMiddle = Math.abs(index - (slices - 1) / 2);
const delay = distanceFromMiddle * config.staggerIn;
window.setTimeout(() => {
layer.style.clipPath = `polygon(0% ${startClipY}%, 100% ${startClipY}%, 100% ${endClipY}%, 0% ${endClipY}%)`;
}, delay);
});
});
window.setTimeout(() => {
imageContainer.classList.add("animate-out");
window.setTimeout(() => {
if (imageContainer.parentElement === trailContainer) {
trailContainer.removeChild(imageContainer);
}
}, config.outDuration);
}, config.imageLifespan);
};
const handleMouseMove = (event: MouseEvent) => {
mousePosRef.current = { x: event.clientX, y: event.clientY };
};
const render = () => {
interpolatedMousePosRef.current.x = mathUtils.lerp(
interpolatedMousePosRef.current.x,
mousePosRef.current.x,
smoothing
);
interpolatedMousePosRef.current.y = mathUtils.lerp(
interpolatedMousePosRef.current.y,
mousePosRef.current.y,
smoothing
);
if (getMouseDistance() > spawnThreshold) {
lastMousePosRef.current = { ...interpolatedMousePosRef.current };
createTrailImage();
}
animationFrameRef.current = requestAnimationFrame(render);
};
const initializeMouse = (event: MouseEvent) => {
mousePosRef.current = { x: event.clientX, y: event.clientY };
interpolatedMousePosRef.current = { x: event.clientX, y: event.clientY };
lastMousePosRef.current = { x: event.clientX, y: event.clientY };
};
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mousemove", initializeMouse, { once: true });
animationFrameRef.current = requestAnimationFrame(render);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mousemove", initializeMouse);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [mounted, configOverride, finalImages, slices, smoothing, spawnThreshold]);
if (!mounted) {
return null;
}
const trail = (
<div
className={cn("pixelated-trail-container", className)}
ref={trailContainerRef}
/>
);
return usePortal ? createPortal(trail, document.body) : trail;
}
export default PixelatedImageTrail;