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

Published: (April 2, 2026 at 11:39 AM EDT)
6 min read
Source: Dev.to

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 filter provides a one‑line blur (blur(Xpx)).
  • Oversized drawing (blurRadius * 2 overflow) 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 mode

Key Points

  • Canvas sizenaturalWidth × naturalHeight (full original resolution).
  • canvas.toDataURL(fileType, 0.95) preserves the original format (JPEG at 95 % quality, PNG lossless).
  • The filename receives a -blurred suffix 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 mode
  • e.preventDefault() on dragover is required – without it the browser handles the drop itself (usually opening 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 a 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.

0 views
Back to Blog

Related posts

Read more »

The Case for Client-Side Developer Tools

Every time you paste a JWT into a decoder, run a regex against a sample string, or convert a color value from HSL to hex in an online tool, you're making a smal...

CipherKit

Introduction I built 77 free developer tools that run 100 % in your browser—no tracking, no backend. If you’re like me, you’ve probably hesitated before pastin...

Execution Context

Bayangkan Execution Context seperti sebuah dapur. Sebelum kamu memulai memasak mengeksekusi kode, kamu perlu ruang kerja, peralatan variabel, dan resep function...