Favicons are tiny but they matter. A sharp, recognizable 32×32 icon in a browser tab signals polish. But most favicon tools either require uploads, email sign-ups, or bloated JavaScript frameworks. I wanted something you can open, use in 10 seconds, and close — with nothing sent to a server.
So I built Favicon Generator: a single HTML file that converts text, emoji, or pasted SVG code into PNG files (16×16 through 180×180) and a multi-size .ico. Everything runs in the browser. Zero upload. Zero tracking.
Live tool → devnestio.pages.dev/favicon-generator/
What it does
- Text or emoji input — type 1–3 characters ("Fg", "JS", "🚀") and the Canvas API renders them live
-
SVG paste — paste any SVG code; it renders via
URL.createObjectURLand scales to fill the canvas - Background & foreground color pickers with hex text inputs (three-char shorthand works)
-
Auto-contrast button — uses WCAG 2.1 relative luminance to pick
#000000or#ffffffautomatically - Border-radius slider — 0% (sharp square) to 50% (perfect circle)
- Font family selector — System sans-serif, Serif, Monospace, Impact
- Manual font-size override — or keep it on "auto" and the tool scales based on glyph count
- 10 one-click presets — language logos (JS, Go, Py, TS) and emoji styles
- Live previews — 256px main canvas, browser-tab mockup (with fake chrome UI), phone home-screen mockup
- Download individual PNGs — 16, 32, 64, 128, 180px buttons
- Download ZIP — all selected sizes bundled, implemented in pure JS (no JSZip, no external library)
-
Download ICO — 16px + 32px images packed into a real
.icobinary, hand-rolled from the ICO spec
The interesting bits
ICO format from scratch
ICO is a binary container format. Modern tools embed PNG data directly inside it (rather than raw BMP), and browsers understand that since IE 10+.
The structure is straightforward:
ICONDIR (6 bytes)
Reserved [0x0000]
Type [0x0001] ← 1 = ICO, 2 = CUR
Count [N]
ICONDIRENTRY × N (16 bytes each)
Width [1 byte] ← 0 means 256
Height [1 byte]
ColorCount [0] ← 0 = no palette (for PNG)
Reserved [0]
Planes [0x0001]
BitCount [0x0020] ← 32 bpp
BytesInRes [uint32] ← size of PNG blob
ImageOffset [uint32] ← byte offset from file start
PNG data × N
The first image offset = 6 + 16 * count. Each subsequent image sits right after the previous one. Simple.
function createIco(images) {
// images = [{width, height, pngData: Uint8Array}]
const count = images.length;
const dirSize = 6 + count * 16;
let offset = dirSize;
const enriched = images.map(img => {
const e = { ...img, offset };
offset += img.pngData.length;
return e;
});
// ... write header, directory, then concatenate PNG blobs
}
Getting the PNG bytes from a Canvas element:
function canvasToPng(canvas) {
const dataUrl = canvas.toDataURL("image/png");
const base64 = dataUrl.split(",")[1];
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
ZIP format from scratch (STORE method)
For the ZIP download I implemented a minimal STORE (no compression) ZIP creator. It is only ~80 lines including CRC32:
Local file header (30 + filename_length bytes)
File data
...repeat for each file...
Central directory entries
End of central directory record
Key signatures to know:
- Local file header:
0x04034B50(PK\x03\x04) - Central directory entry:
0x02014B50(PK\x01\x02) - End of central directory:
0x06054B50(PK\x05\x06)
All multi-byte integers are little-endian. That is the only gotcha.
CRC32 uses the standard IEEE 802.3 polynomial 0xEDB88320 (bit-reversed):
const CRC32_TABLE = (() => {
const t = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++)
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
t[i] = c;
}
return t;
})();
function crc32(data) {
let crc = 0xFFFFFFFF;
for (let i = 0; i < data.length; i++)
crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ data[i]) & 0xFF];
return (crc ^ 0xFFFFFFFF) >>> 0;
}
Test vector: crc32("123456789") should equal 0xCBF43926.
Emoji rendering
Emoji need special handling on Canvas. The trick is skipping fillStyle and using sans-serif (not your custom font), letting the OS emoji font take over:
if (isEmoji(text)) {
ctx.font = `${fontSize}px sans-serif`;
// do not set fillStyle — OS emoji font handles color
} else {
ctx.fillStyle = fgColor;
ctx.font = `bold ${fontSize}px ${fontFamily}`;
}
ctx.fillText(text, size / 2, size / 2 + size * 0.03);
The tiny + size * 0.03 vertical nudge corrects the optical center — Canvas textBaseline: middle tends to sit slightly high for square layouts.
Auto-contrast via WCAG 2.1
function luminance(r, g, b) {
const srgb = [r, g, b].map(v => {
const s = v / 255;
return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
});
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}
function autoContrast(bgHex) {
const rgb = hexToRgb(bgHex);
const lum = luminance(rgb.r, rgb.g, rgb.b);
return lum > 0.179 ? "#000000" : "#ffffff";
}
The threshold 0.179 is derived from the WCAG 2.1 contrast ratio formula — at this luminance, white and black text have roughly equal contrast against the background.
Testing: 136 tests, zero test framework
The tool is one HTML file, so the pure-logic functions (ICO, ZIP, CRC32, color math) are mirrored in test/test.js and run with plain node:
node test/test.js
Test categories:
| Category | Count |
|---|---|
| CRC32 algorithm | 15 |
| ICO binary format | 23 |
| ZIP binary format | 25 |
| Color utilities (hexToRgb, luminance, autoContrast) | 25 |
| Hex validation & normalization | 11 |
| Text & emoji processing | 22 |
| Utility functions (clamp, readU16LE, readU32LE) | 10 |
| Integration (ICO + ZIP end-to-end) | 5 |
| Total | 136 |
Standard test vector sanity:
test("CRC32 of 123456789 = 0xCBF43926", () => {
assert.strictEqual(crc32(new TextEncoder().encode("123456789")), 0xCBF43926);
});
test("ICO dir entry 0: offset = 6 + 16 (1 image)", () => {
const ico = createIco([{ width: 16, height: 16, pngData: fakePng }]);
assert.strictEqual(readU32LE(ico, 18), 22); // 6 header + 16 dir entry
});
Tech used
- Canvas API — rendering and PNG export
- Blob + URL.createObjectURL — SVG to Canvas pipeline and file downloads
- DataView — writing ICO and ZIP binary structures
- TextEncoder — filename bytes for ZIP
- Intl.Segmenter — accurate grapheme cluster count (emoji sequences like 🏳️🌈)
No build step. No bundler. No npm install for the browser. Open the file, it works.
Try it
- Tool: devnestio.pages.dev/favicon-generator/
- Hub (25 tools total): devnestio.pages.dev
Happy favicon-ing. 🎨
Top comments (0)