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;
}
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
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");
}
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" }
];
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)