Skip to main content
Back to BlogGuide

Why Your Favicon Looks Blurry (And the Downscale Fix)

Single-step canvas downscaling destroys detail at 16×16. Why most favicon generators produce blurry icons, and how stepwise halving fixes it.

August 20266 min read

You upload a razor-sharp 1024×1024 logo. The generator spits out a 16×16 favicon. And the result in the browser tab is an unrecognizable smudge. If that sounds familiar, the problem usually isn't your logo — it's how the icon was shrunk. Here's the computer graphics reason blurry favicons happen, and the resize technique that fixes them.

Why one-step resizing destroys small icons

Most favicon generators (and most copy-pasted canvas code) do this:

ctx.drawImage(img, 0, 0, 16, 16) // 1024px source → 16px target, one hop

A single drawImage call that reduces an image 64× uses simple bilinear interpolation. For each of the 256 output pixels, the browser samples a tiny neighborhood of the source — meaning roughly 99.98% of your input pixels are thrown away without being averaged. Fine detail like thin strokes, letterforms, and 1px gaps doesn't get “blended down”; it gets sampled at random spots, which is why text turns to porridge and logos develop muddy blobs.

This is a classic graphics problem with a known solution: downscale in steps, halving each time (1024 → 512 → 256 → 128 → 64 → 32 → 16). Each 2× step is small enough that bilinear sampling averages a meaningful neighborhood, so detail is progressively merged instead of skipped. It's the same idea behind mipmaps in 3D rendering and imageSmoothingQuality: "high" in canvas — but explicitly chaining the halving steps gives consistent results across browsers.

The fix: stepwise halving in canvas

function drawDownscaled(ctx, img, targetW, targetH) {
  let source = img
  let w = img.naturalWidth
  let h = img.naturalHeight

  // Halve repeatedly while both dimensions stay above the target
  while (Math.floor(w / 2) >= targetW && Math.floor(h / 2) >= targetH) {
    const step = document.createElement('canvas')
    step.width = Math.floor(w / 2)
    step.height = Math.floor(h / 2)
    const stepCtx = step.getContext('2d')
    stepCtx.imageSmoothingEnabled = true
    stepCtx.imageSmoothingQuality = 'high'
    stepCtx.drawImage(source, 0, 0, w, h, 0, 0, step.width, step.height)
    source = step
    w = step.width
    h = step.height
  }

  // Final hop is now at most ~2x, which bilinear handles well
  ctx.imageSmoothingQuality = 'high'
  ctx.drawImage(source, 0, 0, w, h, 0, 0, targetW, targetH)
}

The loop guarantees the last reduction is never more than ~2× per axis — the range where interpolation actually works. On a detailed logo the difference is night and day: strokes stay connected, counters (the holes in letters) survive, and edges keep contrast instead of dissolving into gray.

Even a perfect resize can't save a bad source

Two source-side mistakes cause most remaining blur:

  • Starting from a raster that's already small. A 64×64 source upscaled to 512 and back down gains nothing — always start from the largest version of your logo, or better, a vector original.
  • Designing for 512 and hoping it reads at 16. Great tab icons are designed for 16px: drop fine detail, thicken strokes to at least 1/16th of the canvas, and use one or two colors. If your logo has a wordmark, use just the mark or the first letter at favicon sizes.

And if you want to skip raster resizing entirely, an SVG favicon is resolution-independent — the browser renders the vector at exactly 16px, no interpolation involved.

How to verify your favicon is actually sharp

  1. Download the generated favicon-16x16.png and open it in an image viewer at 400% zoom. You want defined edges, not gray mush.
  2. Compare it side-by-side with your source logo squinted at arm's length — the silhouette should still match.
  3. Check it in a real browser tab, both on light and dark themes, and against other tabs to see if it's distinguishable.

We fixed this in our generator

When we audited our own pipeline against this exact failure mode, we replaced single-step resizing with progressive halving (plus imageSmoothingQuality: "high") across every output size. It's why icons generated at Favicon.one hold up at 16px where other tools smear. If you're comparing generators, this is the test: upload a detailed logo with thin strokes and small text, and look at the 16×16 output first.

More on the pipeline: how to convert a PNG to a favicon properly, and every dimension you need in the favicon size guide.

Ready to generate your favicon?

Upload your image and get a complete favicon package in seconds. Free, private, and processed right in your browser.