The format in one sentence

An ICO file is a container — a small header, a directory of entries, and then one or more concatenated image blobs (each either BMP-formatted or PNG-formatted). Think of it as a zip file without compression or metadata: just "here are the images this container holds, in order."

That container design is the whole point. A single .ico file can hold a 16×16 version, a 32×32 version, a 48×48 version, and a 256×256 version all at once — so the browser (or the Windows shell) can pick whichever resolution it needs without a separate request.

The bytes, in order

A minimal single-image ICO file looks like this, top to bottom:

Offset  Bytes  Field                 Value
──────────────────────────────────────────────
  0     2      Reserved              0x0000
  2     2      Image type            0x0001 (1 = ICO, 2 = CUR cursor)
  4     2      Image count           N images
──────────────────────────────────────────────          ← ICONDIR header, 6 bytes
  6     1      Width                 0-255 (0 means 256)
  7     1      Height                0-255 (0 means 256)
  8     1      Palette count         0 if no palette
  9     1      Reserved              0x00
 10     2      Color planes          0 or 1
 12     2      Bits per pixel        1, 4, 8, 24, or 32
 14     4      Image data size       Length of this entry's blob
 18     4      Image data offset     Where the blob starts in the file
──────────────────────────────────────────────          ← ICONDIRENTRY, 16 bytes (repeat N times)
 22     …      Image blob data       BMP or PNG bytes
──────────────────────────────────────────────

The top-level ICONDIR is exactly 6 bytes. Then one ICONDIRENTRY (16 bytes) per image. Then the image blobs themselves, at the offsets the entries point to.

With two images, the layout is:

[6 bytes ICONDIR]
[16 bytes ICONDIRENTRY for image 1, offset 38]
[16 bytes ICONDIRENTRY for image 2, offset 38+img1.size]
[image 1 data]
[image 2 data]

Nothing in the spec requires the entries to be sorted by size, nor does it require the blobs to appear in entry order. In practice most tools write them sorted smallest-to-largest, and browsers assume as much when picking one. If you hand-craft ICOs you can write them in any order; expect some clients to silently pick the wrong one.

The "0 means 256" dimension trap

Width and height are each a single byte. That gives you values 0-255. But favicons at 256×256 exist — that's the standard size for high-DPI displays. The workaround baked into the format: a value of 0x00 in the width or height field means 256.

So an ICO header like 00 00 ... could mean "a 0×0 image, obviously a bug" OR "a 256×256 image, legitimate." Every correct parser has to hardcode the rule: if width == 0 then width = 256.

Real-world consequence: a surprising number of tools get this wrong. I've seen image inspectors report ".ico contains a 0×0 image" when the bytes are actually a valid 256×256. If you're writing an ICO parser, the special-case is the first thing to test.

BMP-in-ICO: the DIB part

The "normal" way to store an image inside an ICO is as a BMP — specifically, as a BITMAPINFOHEADER (DIB) followed by pixel data. There's a critical difference from a standalone .bmp file: the BITMAPFILEHEADER (the 14-byte "BM" prefix at the top of a real BMP file) is stripped. The ICO format doesn't use it. The image blob starts straight at the DIB header.

That's why you can't just "extract" an ICO image blob and open it as a BMP without reconstructing the file header first. Most .ico tools silently add the 14 bytes back when exporting.

There's a second critical quirk. For ICO-embedded BMPs, the DIB header's height field is doubled. A 32×32 icon has biHeight = 64 in its DIB header. This is because the image blob includes both the color data (32 rows) and an AND mask (another 32 rows of 1-bit alpha) concatenated. The mask was essential on Windows 95 (no alpha channel support). It's vestigial today but removing it would break every old parser, so it stays.

Modern 32-bit BMP-in-ICO images have full alpha in the main color data and should have an all-zero AND mask. But you still write the bytes, and you still declare biHeight = 2 * actual_height.

PNG-in-ICO: the modern path

Since Windows Vista (2006) and Firefox 3 (2008), ICO files have been allowed to embed PNGs directly instead of BMPs. You just drop a valid PNG file into the image blob, and the parser detects it by the magic bytes (89 50 4E 47 at the start of the blob).

PNG-in-ICO has two big advantages: smaller file sizes (PNG deflate-compresses pixel data, BMP doesn't), and real alpha channels without the DIB header's height-doubling quirk. For the 256×256 size, PNG is essentially mandatory — a 256×256 32-bit BMP with AND mask is 262 KB, the PNG equivalent is maybe 20 KB.

The convention today: write 16×16, 32×32, 48×48 as BMP (smaller for tiny pixel-accurate icons), and 256×256 as PNG (massive savings on the large version). That's exactly what Favicon Foundry does; it's also what the Windows icon tools do by default.

Why favicons are still ICO in 2026

HTML supports <link rel="icon" href="/favicon.png">. And .svg. And .webp. All of these work in every evergreen browser. So why do we keep shipping .ico?

Three reasons, in decreasing order of defensibility:

  • The implicit fallback. Browsers still request /favicon.ico when a page has no <link rel="icon"> (or when the one linked fails to load). If you don't ship one, your logs fill with 404s and some desktop tools (browser bookmark bars, RSS readers, iMessage link previews) show a broken-icon placeholder. Shipping favicon.ico at the root is a 20-year-old browser habit that persists for exactly these fallback reasons.
  • Legacy bookmark/tab behavior. Windows taskbar pins, pinned tabs in some browsers, and a handful of old RSS clients still look for ICO specifically. Usually these are not evergreen software and won't ever be updated.
  • The multi-resolution story. SVG solves this elegantly — one scalable file handles every size. But browsers need raster fallbacks for specific sizes (taskbar icons at 16×16 on a 1× display are better as hand-drawn bitmaps than downsampled SVGs, because of subpixel anchoring). A multi-resolution ICO still does that job more efficiently than multiple separate PNG <link> tags.

The right modern configuration is to ship all three: favicon.ico (root fallback), <link rel="icon" type="image/svg+xml" href="/favicon.svg"> (SVG for modern browsers), and an apple-touch-icon.png at 180×180 (for iOS home screen). Browsers pick the best one; non-browser tools get the ICO fallback. This is exactly what the HTML5 favicon guide recommends and what Favicon Foundry generates.

Eight gotchas worth knowing

The "invalid BMP" error from image libraries. If you extract the image blob from an ICO and hand it to an image library expecting a BMP, you'll get "invalid magic bytes" — you need to prepend a 14-byte BITMAPFILEHEADER first. The correct header points to the pixel data offset, which you calculate from the DIB header's biSize field plus palette entries if present.

Height doubling forgotten. If you're writing BMP-in-ICO from scratch, biHeight = 2 * your_height. If you write your height directly, the AND mask gets read as pixel data and the image looks shifted/corrupted in half the parsers.

The AND mask pixel format. Each row of the AND mask is a 1-bit bitmap (1 = transparent, 0 = opaque), padded to a 4-byte boundary. A 32×32 mask is 128 bytes (32 rows × 4 bytes per row). Skip the padding and your file parses wrong.

BMP rows are bottom-to-top. BMP stores pixel rows starting from the bottom of the image (a DIB legacy from when it was meant to be directly blitted to screen memory that grew upward). If your image appears upside-down, that's why. ICOs inherit this; PNGs inside ICOs do not.

Palette handling. For BPP values of 1, 4, or 8, the image has a palette (a table of RGBA values) that precedes the pixel data. Count = 2^bpp. Modern 32-bit icons have no palette (biClrUsed = 0). If you encounter an 8-bit palette-indexed ICO in the wild, expect edge cases.

Size field vs actual size. The ICONDIRENTRY includes a 4-byte field for the image's data size. You can use it to validate, or to skip past an image you don't care about. Some buggy tools write zero here and rely on the offset of the next entry for implicit size. Both your writer and your reader should handle this defensively.

The CUR cursor variant. Same format, except image type is 0x0002 and the two reserved-in-ICO bytes (palette count + 0x00) are reinterpreted as hotspot X and Y. If you're writing a strict ICO parser, check the type field and error out on CUR files — otherwise you'll misread hotspot coordinates as palette info.

Size ordering isn't guaranteed. The spec doesn't require entries to be sorted. Most tools sort smallest-to-largest, but if you're picking "the best size for my display," iterate all entries and pick by actual width/height — don't assume entry 0 is the smallest.

Companion tool

If you need to build favicons for a site right now: Favicon Foundry. Drop in a source image (PNG, JPG, or SVG), get back a full favicon package — ICO with the 16/32/48 BMP sizes plus 256 PNG, standalone SVG, apple-touch-icon at 180, and the copy-paste HTML for your <head>. Everything generated in your browser; the source image never uploads.

The ICO writer in Favicon Foundry implements the BMP-with-doubled-height and PNG-embedded variants described above. You can test this by generating an ICO and then pasting it into ICO Inspector — it will show you the container structure, the ICONDIR, each ICONDIRENTRY, and a byte-level hex dump with the 6-byte ICONDIR header and each 16-byte entry record highlighted. Hand-rolled an ICO yourself? The Inspector will flag any spec violations it finds (non-doubled DIB height, unexpected reserved bytes, overlapping data ranges, invalid dimensions).