Building a Browser-Based Image Blur Tool with Canvas API (No Libraries)
Source: Dev.to
Blurring an image in the browser sounds like it should need a library. It doesn’t.
The Canvas 2D API has a built‑in filter property that accepts the same CSS filter syntax you already know — including blur(). This post shows how to build a fully client‑side image‑blur tool in Next.js with blur presets, a custom radius slider, and edge‑bleed‑free download output.
Live tool
ultimatetools.io/tools/image-tools/blur-image/
The core: ctx.filter = "blur(Xpx)"
The entire blur effect is one line:
ctx.filter = `blur(${blurRadius}px)`;
ctx.drawImage(img, 0, 0);ctx.filter accepts any CSS filter string. Setting it before drawImage applies the filter to the drawn pixels, producing a blurred image on the canvas that’s ready to export. This works in all modern browsers and requires zero dependencies.
The edge‑bleeding problem
When you blur an image with a large radius, the edges fade to transparent because the blur algorithm needs pixels outside the canvas boundary. A 400 × 300 image blurred at 25 px will have a soft, faded border on all four sides—undesirable for a download.
The fix: draw the image oversized, then let the canvas clip it
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d")!;
ctx.filter = `blur(${blurRadius}px)`;
// Draw larger than the canvas by blurRadius on each side
const overflow = blurRadius * 2;
ctx.drawImage(
img,
-overflow, // x: start left of canvas
-overflow, // y: start above canvas
img.naturalWidth + overflow * 2, // wider than canvas
img.naturalHeight + overflow * 2 // taller than canvas
);By drawing the image blurRadius * 2 pixels outside each edge, the blur algorithm has real pixels to work with at the boundary. The canvas automatically clips anything drawn outside its bounds, so the output is full‑bleed with no faded edges. The * 2 multiplier comfortably covers radii up to ~50 px; increase it for very large radii.
Preset system
Three named presets map to fixed blur values:
type Preset = "subtle" | "medium" | "heavy" | "custom";
const PRESETS = [
{ id: "subtle", label: "Subtle", desc: "Light blur", value: 3 },
{ id: "medium", label: "Medium", desc: "Balanced blur", value: 10 },
{ id: "heavy", label: "Heavy", desc: "Strong blur", value: 25 },
];Selecting a preset
const handlePresetChange = (p: Preset) => {
setPreset(p);
const found = PRESETS.find((x) => x.id === p);
if (found) setBlurRadius(found.value);
};Moving the custom slider
const handleSliderChange = (val: number) => {
setBlurRadius(val);
setPreset("custom");
};The slider switches the UI to the "custom" preset while preserving the numeric radius, ensuring preset buttons deselect when the user drags the slider manually.
Live preview with CSS filter
The preview uses CSS (instant, GPU‑accelerated) while the download uses Canvas for pixel‑accurate output:
The “ element updates at 60 fps as the slider moves, with no canvas re‑processing required. When the user clicks Download, the canvas routine (described below) produces an edge‑correct, full‑resolution file.
File loading with FileReader
Images are loaded client‑side:
const processFile = (file: File) => {
if (!file.type.startsWith("image/")) return;
setFileType(file.type);
setFileName(file.name.replace(/\.[^.]+$/, "")); // strip extension
const reader = new FileReader();
reader.onload = (e) => {
setImage(e.target?.result as string); // base64 data URL
};
reader.readAsDataURL(file);
};readAsDataURL returns a base64‑encoded URL that can be assigned directly to img.src. The original MIME type (image/jpeg, image/png, …) is stored so the download uses the same format.
Storing the loaded image in a ref
useEffect(() => {
if (!image) return;
const img = new window.Image();
img.onload = () => { imgRef.current = img; };
img.src = image;
}, [image]);The ref gives access to .naturalWidth and .naturalHeight, needed for setting canvas dimensions at full resolution.
Download
const download = () => {
const img = imgRef.current;
if (!img) return;
const canvas = document.createElement("canvas");
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext("2d")!;
ctx.filter = `blur(${blurRadius}px)`;
const overflow = blurRadius * 2;
ctx.drawImage(
img,
-overflow,
-overflow,
img.naturalWidth + overflow * 2,
img.naturalHeight + overflow * 2
);
const ext = fileType === "image/jpeg" ? "jpg" : fileType.split("/")[1];
const link = document.createElement("a");
link.href = canvas.toDataURL(`image/${ext}`);
link.download = `${fileName}_blurred.${ext}`;
link.click();
};The canvas produces a data URL with the correct MIME type, and a temporary “ element triggers the download.
Summary
- Canvas
filterprovides a one‑line blur (blur(Xpx)). - Oversized drawing (
blurRadius * 2overflow) eliminates edge bleeding. - Presets + custom slider give users quick choices and fine control.
- CSS preview offers instant feedback; Canvas download guarantees pixel‑perfect, edge‑correct output.
All of this runs entirely in the browser—no external libraries required. Enjoy building your own blur tool!
```tsx
// Example download function
const downloadBlurred = (canvas: HTMLCanvasElement, fileName: string, ext: string, fileType: string) => {
const link = document.createElement('a');
link.href = canvas.toDataURL(fileType, 0.95);
link.download = `${fileName}-blurred.${ext}`;
link.click();
};Fullscreen Controls
Enter fullscreen mode
Exit fullscreen modeKey Points
- Canvas size –
naturalWidth × naturalHeight(full original resolution). canvas.toDataURL(fileType, 0.95)preserves the original format (JPEG at 95 % quality, PNG lossless).- The filename receives a
-blurredsuffix to distinguish it from the original. - No server is involved – the blob URL is created and clicked entirely in the browser.
Drag and Drop
Standard drag‑and‑drop with onDragOver, onDragLeave, onDrop:
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files?.[0]) processFile(e.dataTransfer.files[0]);
};Enter fullscreen mode
Exit fullscreen modee.preventDefault()ondragoveris required – without it the browser handles the drop itself (usually opening the file in a new tab).
Key Takeaways
ctx.filter = "blur(Xpx)"beforedrawImageis all you need for canvas blur – no library required.- Draw the image with
blurRadius * 2overflow on each side to eliminate edge fading. - Use CSS
filter: blur()for a live preview – it’s GPU‑accelerated and updates instantly. FileReader.readAsDataURL→new Image()is the reliable pattern for loading user files into canvas operations.- Store the original
fileTypeto preserve format on download.
Try it:
ultimatetools.io/tools/image-tools/blur-image/ – no upload, runs entirely in your browser.