Skip to content
← components

Image Pixelator

new

Drop an image and pixelate it — drawn tiny then scaled back up with smoothing off, so it renders as crisp blocks. Block-size presets, full-res PNG export, transparency kept.

drop your own image, pick a block size, export the PNG

Usage

example.tsx
import { Pixelator } from "~/components/effects/pixelator";
<Pixelator defaultSrc="/avatar.png" />

How it works

Pixelation is two drawImage calls and one flag. First the picture is drawn into a tiny canvas, say a sixteenth of its size, with smoothing on so it shrinks cleanly and averages each region into one color. Then it's drawn back up to full size with imageSmoothingEnabled = false, so each of those small samples becomes a hard square instead of being interpolated. The block size is just the divisor.

Everything runs at the image's native resolution, so the PNG you export is crisp at full size rather than an upscaled screenshot. Alpha survives the round trip, so cut-out subjects stay cut out. I kept the original's mono, drop-to-upload feel and added a sample so the demo isn't empty.

pixelate.ts
// Draw the image tiny, then scale it back up with smoothing OFF.
const sw = Math.round(w / block); // e.g. 1/16th the size
const sh = Math.round(h / block);
const tmp = document.createElement("canvas");
tmp.width = sw; tmp.height = sh;
tmp.getContext("2d").drawImage(img, 0, 0, sw, sh); // shrink (smooth)
ctx.imageSmoothingEnabled = false; // the whole trick
ctx.drawImage(tmp, 0, 0, sw, sh, 0, 0, w, h); // blow it back up

Props

proptypedefaultdescription
defaultSrcstringImage to load on mount (e.g. a sample).
classNamestringExtra classes on the wrapper.

Recreated from Pixelate Me (@rauchg, design by Taras Donchenko).

The full implementation. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).

components/effects/pixelator.tsx
import { useCallback, useEffect, useRef, useState } from "react";
import { Plus, Download, Repeat, Trash2 } from "lucide-react";
import { cn } from "~/lib/utils";
const PRESETS = [4, 8, 16, 32, 64];
export interface PixelatorProps {
/** Optional image to load on mount (e.g. a sample). */
defaultSrc?: string;
className?: string;
}
/**
* Drop or pick an image and pixelate it: the image is drawn tiny, then scaled
* back up with image smoothing off, so it renders as crisp blocks. Pick a block
* size and export the full-resolution result as a PNG. Transparency is kept.
* Recreated from rauchg's "Pixelate Me" (design by Taras Donchenko).
*/
export function Pixelator({ defaultSrc, className }: PixelatorProps) {
const [img, setImg] = useState<HTMLImageElement | null>(null);
const [block, setBlock] = useState(16);
const [dragging, setDragging] = useState(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const loadSrc = useCallback((src: string) => {
const im = new Image();
im.crossOrigin = "anonymous";
im.onload = () => setImg(im);
im.src = src;
}, []);
useEffect(() => {
if (defaultSrc) loadSrc(defaultSrc);
}, [defaultSrc, loadSrc]);
// Render the pixelated image at native resolution (so export is full-res).
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !img) return;
const w = img.naturalWidth;
const h = img.naturalHeight;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const sw = Math.max(1, Math.round(w / block));
const sh = Math.max(1, Math.round(h / block));
const tmp = document.createElement("canvas");
tmp.width = sw;
tmp.height = sh;
const tctx = tmp.getContext("2d");
if (!tctx) return;
tctx.imageSmoothingEnabled = true;
tctx.drawImage(img, 0, 0, sw, sh);
ctx.clearRect(0, 0, w, h);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(tmp, 0, 0, sw, sh, 0, 0, w, h);
}, [img, block]);
function onFiles(files: FileList | null) {
const f = files?.[0];
if (!f || !f.type.startsWith("image/")) return;
loadSrc(URL.createObjectURL(f));
}
function exportPng() {
const c = canvasRef.current;
if (!c) return;
const a = document.createElement("a");
a.download = `pixelated-${block}px.png`;
a.href = c.toDataURL("image/png");
a.click();
}
function clear() {
setImg(null);
if (inputRef.current) inputRef.current.value = "";
}
return (
<div
className={cn(
"flex flex-col items-center gap-6 rounded-xl border border-[hsl(var(--border))] bg-black p-6 font-mono text-xs uppercase tracking-wider text-white",
className
)}
onDragOver={(e) => {
e.preventDefault();
setDragging(true);
}}
onDragLeave={() => setDragging(false)}
onDrop={(e) => {
e.preventDefault();
setDragging(false);
onFiles(e.dataTransfer.files);
}}
>
<input
ref={inputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => onFiles(e.target.files)}
/>
{!img ? (
<button
type="button"
onClick={() => inputRef.current?.click()}
className={cn(
"flex h-72 w-full flex-col items-center justify-center gap-4 rounded-lg border border-dashed transition-colors",
dragging
? "border-white/60 bg-white/5"
: "border-white/15 hover:border-white/30"
)}
>
<span className="flex size-14 items-center justify-center rounded-full border border-white/20">
<Plus className="size-6" />
</span>
<span className="max-w-[16rem] text-center text-white/50">
Add your picture that you want to pixelate
</span>
</button>
) : (
<>
<div className="flex min-h-72 w-full items-center justify-center">
<canvas
ref={canvasRef}
className="max-h-72 max-w-full"
style={{ imageRendering: "pixelated" }}
aria-label="Pixelated image"
/>
</div>
<div className="flex flex-wrap items-center justify-center gap-4">
{PRESETS.map((p) => (
<button
key={p}
type="button"
onClick={() => setBlock(p)}
className={cn(
"transition-colors",
block === p ? "text-white" : "text-white/35 hover:text-white/70"
)}
>
{p}px
</button>
))}
</div>
<div className="flex flex-wrap items-center justify-center gap-2.5">
<Action onClick={exportPng} icon={<Download className="size-3.5" />}>
Export PNG
</Action>
<Action
onClick={() => inputRef.current?.click()}
icon={<Repeat className="size-3.5" />}
>
Replace media
</Action>
<Action onClick={clear} icon={<Trash2 className="size-3.5" />}>
Clear all
</Action>
</div>
</>
)}
</div>
);
}
function Action({
onClick,
icon,
children,
}: {
onClick: () => void;
icon: React.ReactNode;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
className="inline-flex items-center gap-2 rounded-md border border-white/15 px-3 py-2 text-white/80 transition-colors hover:border-white/30 hover:bg-white/5 hover:text-white"
>
{icon}
{children}
</button>
);
}