DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Build a Favicon Generator in the Browser with canvas.toBlob and No Server

A favicon is that tiny square in the browser tab, the bookmark, the phone home screen. Making one used to mean a round-trip through some upload site. It never needed to: a browser can already draw and encode images. Give it a glyph or an uploaded picture, rasterise it fresh at each standard size on a <canvas>, and encode PNGs with toBlob — no library, no server, nothing leaves the page. Here is the whole pipeline.

One design object, one render function

Hold the entire icon as plain data: a mode, the glyph text, colours, a font, a shape, and — for uploads — a decoded Image. Everything downstream reads only this object, so a control change is just a field write plus a redraw.

The one function everything calls is renderAt(canvas, size). It sizes the canvas, clips to the shape, paints the backdrop, then draws either a centred glyph or a cover-fit image. Because it takes the size as an argument, the 16px tab preview and the 512px download run the same code.

function renderAt(canvas, size) {
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, size, size);
  ctx.save();
  clipShape(ctx, size, design.shape); ctx.clip();   // mask to square/rounded/circle
  ctx.fillStyle = design.bg; ctx.fillRect(0, 0, size, size);
  if (design.mode === "image" && design.img) drawCover(ctx, design.img, size);
  else                                        drawGlyph(ctx, size);
  ctx.restore();
  return canvas;
}
Enter fullscreen mode Exit fullscreen mode

Rasterise per size — don't shrink one big image

This is the single idea that separates a crisp favicon from a blurry one. The naive way renders one 512px icon and lets the browser scale it down to 16px, which turns a monogram to mush. A 16×16 has only 256 pixels to say everything with, so it wants its own render. Drawing fresh at each size lets the glyph stay legible where it counts:

renderAt(tabCanvas, 16);
renderAt(previewCanvas, 128);
renderAt(exportCanvas, 512);   // three fresh rasters, not one downscale
Enter fullscreen mode Exit fullscreen mode

Shapes are just clip paths — a circle is an arc, a rounded square is four arcTo corners with the radius scaled to the size. A glyph is centred with textAlign: "center" and textBaseline: "middle" so one fillText lands centred at any size. An upload is read locally with FileReader.readAsDataURL (a data URI, never uploaded) and cover-fit by scaling to the larger of the two ratios.

Encode a PNG and download it

The export is three browser calls, no library: toBlob encodes the canvas as a real PNG, URL.createObjectURL wraps that blob in a URL, and a hidden <a download> click saves it. Revoke the URL afterwards so it isn't leaked.

function downloadPNG(size, file) {
  const c = document.createElement("canvas");
  renderAt(c, size);
  c.toBlob(blob => {
    const url = URL.createObjectURL(blob);
    const a = Object.assign(document.createElement("a"), { href: url, download: file });
    a.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }, "image/png");
}
Enter fullscreen mode Exit fullscreen mode

One table keeps the tags in sync

A single OUTPUTS table drives the downloads, the <link> tags, and the manifest, so they can never drift apart. 16/32/48 are the classic favicon sizes, 180 is Apple's touch icon, 192 and 512 are the Android / PWA icons declared in site.webmanifest.

const OUTPUTS = [
  { size: 16,  file: "favicon-16x16.png",           rel: "icon" },
  { size: 32,  file: "favicon-32x32.png",           rel: "icon" },
  { size: 48,  file: "favicon-48x48.png",           rel: "icon" },
  { size: 180, file: "apple-touch-icon.png",        rel: "apple-touch-icon" },
  { size: 192, file: "android-chrome-192x192.png",  rel: "manifest" },
  { size: 512, file: "android-chrome-512x512.png",  rel: "manifest" }
];
Enter fullscreen mode Exit fullscreen mode

A few facts worth keeping: PNG has beaten multi-size .ico — every current browser accepts a plain PNG via <link rel="icon">, so .ico is now a niche compatibility trick. Apple ignores rel="icon" and wants rel="apple-touch-icon" at 180×180, rounding the corners itself. Android reads the 192/512 icons from the manifest, where purpose: "any maskable" tells the launcher it may crop into a circle or squircle — so keep the important art in the centre safe zone.

The browser was always the image tool. Build your own icon and copy the tags here: https://dev48v.infy.uk/solve/day58-favicon-generator.html

Top comments (0)