DEV Community

Cover image for Building a Browser-Based Image Blur Tool with Canvas API (No Libraries)
Shaishav Patel
Shaishav Patel

Posted on

Building a Browser-Based Image Blur Tool with Canvas API (No Libraries)

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 covers how I built a fully client-side image blur tool in Next.js with blur presets, a custom radius slider, and edge-bleed-free download output.

The 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);
Enter fullscreen mode Exit fullscreen mode

ctx.filter accepts any CSS filter string. Setting it before drawImage applies the filter to the drawn pixels. The result is the blurred image rendered onto the canvas, ready to export.

This works in all modern browsers and requires zero dependencies.


The edge bleeding problem

There's a catch. When you blur an image and the blur radius is large, the edges of the image fade to transparent — the blur algorithm needs pixels outside the canvas boundary, and there are none, so it blends with transparency (or black).

A 400×300 image blurred at 25px radius will have a soft, faded border on all four sides. That looks wrong when downloaded.

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
);
Enter fullscreen mode Exit fullscreen mode

By drawing the image blurRadius * 2 pixels outside each edge, the blur algorithm has real pixels to work with at the boundary. The canvas clips anything drawn outside its bounds, so the output is full-bleed with no faded edges.

The multiplier * 2 gives enough overflow for the blur kernel at any radius up to 50px. For very high radii you could increase this, but blurRadius * 2 covers the typical range.


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 },
];
Enter fullscreen mode Exit fullscreen mode

Selecting a preset sets both the preset state and the blurRadius:

const handlePresetChange = (p: Preset) => {
    setPreset(p);
    const found = PRESETS.find((x) => x.id === p);
    if (found) setBlurRadius(found.value);
};
Enter fullscreen mode Exit fullscreen mode

Moving the custom slider switches to "custom" preset while keeping the numeric radius:

const handleSliderChange = (val: number) => {
    setBlurRadius(val);
    setPreset("custom");
};
Enter fullscreen mode Exit fullscreen mode

This way the preset buttons correctly deselect when the user drags the slider manually.


Live preview with CSS filter

The download uses Canvas for pixel-accurate output, but the preview uses CSS — it's instant and requires no canvas work:

<img
    src={image}
    alt="Preview"
    style={{ filter: `blur(${blurRadius}px)` }}
/>
Enter fullscreen mode Exit fullscreen mode

CSS filter: blur() on an <img> tag renders the blur in real-time as the slider moves. No re-processing, no canvas — just GPU-accelerated CSS. The preview updates at 60fps while dragging.

The download then uses the canvas approach for edge-correct, full-resolution output.


File loading with FileReader

Images are loaded client-side using FileReader:

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);
};
Enter fullscreen mode Exit fullscreen mode

readAsDataURL gives a base64-encoded data URL that can be set directly as img.src. The original file type (image/jpeg, image/png, etc.) is stored so the download uses the same format.

The <img> element is then loaded into a ref for use at download time:

useEffect(() => {
    if (!image) return;
    const img = new window.Image();
    img.onload = () => { imgRef.current = img; };
    img.src = image;
}, [image]);
Enter fullscreen mode Exit fullscreen mode

This keeps a reference to the loaded HTMLImageElement with .naturalWidth and .naturalHeight available — 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(fileType, 0.95);
    link.download = `${fileName}-blurred.${ext}`;
    link.click();
};
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Canvas is sized to naturalWidth × naturalHeight — full original resolution
  • canvas.toDataURL(fileType, 0.95) preserves the original format (JPEG at 95% quality, PNG lossless)
  • The filename gets a -blurred suffix to distinguish from the original
  • No server 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 mode

e.preventDefault() on dragover is required — without it the browser handles the drop itself (usually opens the file in a new tab).


Key takeaways

  • ctx.filter = "blur(Xpx)" before drawImage is all you need for canvas blur — no library required
  • Draw the image with blurRadius * 2 overflow on each side to eliminate edge fading
  • Use CSS filter: blur() for live preview — it's GPU-accelerated and updates instantly
  • FileReader.readAsDataURLnew Image() is the reliable pattern for loading user files into canvas operations
  • Store the original fileType to preserve format on download

Try it: ultimatetools.io/tools/image-tools/blur-image/ — no upload, runs entirely in your browser.

Top comments (0)