Icon Sizes for the Manifest and Action

Which icon sizes an MV3 extension needs: 16, 32, 48 and 128 px for icons, 16 and 32 for the action, store and Safari assets, high-DPI behaviour, a build script that generates them, and setIcon at runtime.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

The toolbar icon looks fuzzy on a Retina display, the extensions page shows a pixelated logo, and the store listing complains about a missing 128-pixel image. Extension icons appear in at least five places at different sizes and densities, and the manifest has two separate keys for them. Supplying one 128-pixel PNG and letting the browser scale it works, in the sense that something appears — and looks noticeably worse than every neighbouring extension. This guide lists the sizes each surface needs and builds them automatically. It belongs to the manifest keys reference.

Where each icon appears

The top-level icons key identifies the extension: Chrome uses the 128-pixel image in the install dialog and the Web Store, the 48-pixel image on chrome://extensions, and 16 and 32 pixels for favicons of extension pages and in context menus. The action.default_icon key is the toolbar button, drawn at 16 CSS pixels — which means 16 physical pixels on a standard display and 32 on a 2× display. When an exact size is missing, the browser scales the nearest larger one down (or smaller one up), and small downscaled icons lose their crispness because fine detail falls between pixels. Supplying every size, each drawn or at least hinted for that size, is the only way to get a sharp result everywhere.

Icon sizes by surfaceWhich manifest key and pixel sizes Chrome uses for the toolbar, extensions page, install dialog, store listing and context menus.SurfaceManifest keySizes usedToolbar buttonaction.default_icon16, 32 (2×)Extensions pageicons48, 96 (2×)Install dialogicons128Web Storeicons + listing128Context menu / faviconicons16, 32
The toolbar needs 16 and 32; the identity icons need 16, 32, 48 and 128.

Step-by-step: a complete icon set

1. Declare every size in both keys

 1{
 2  "icons": {
 3    "16": "icons/16.png",
 4    "32": "icons/32.png",
 5    "48": "icons/48.png",
 6    "128": "icons/128.png"
 7  },
 8  "action": {
 9    "default_icon": { "16": "icons/action-16.png", "32": "icons/action-32.png" }
10  }
11}

Execution context: the manifest. Chrome accepts PNG, and also BMP, GIF, ICO and JPEG, but not SVG for these keys — use PNG with transparency. Keeping separate action files lets you design the toolbar glyph for 16 pixels (often a simplified mark) while the identity icon carries the full logo. Adding 96 for the extensions page on high-density displays is harmless and helps.

2. Generate the sizes from a vector source

 1// scripts/icons.mjs — build-time rasterisation
 2import sharp from "sharp";
 3const SIZES = [16, 32, 48, 96, 128];
 4for (const size of SIZES) {
 5  await sharp("art/logo.svg", { density: 384 })
 6    .resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
 7    .png({ compressionLevel: 9 })
 8    .toFile(`dist/icons/${size}.png`);
 9}
10for (const size of [16, 32]) {
11  await sharp("art/action-glyph.svg", { density: 384 }).resize(size, size).png().toFile(`dist/icons/action-${size}.png`);
12}

Execution context: Node at build time. Rendering each size directly from the vector at a high density gives cleaner edges than resizing a large PNG. For the 16-pixel sizes, a separately drawn glyph — thicker strokes, fewer details — reads far better than the full logo shrunk; check it at 100% zoom on a standard display, not on a Retina screen where it looks deceptively good.

From one vector to every iconA logo SVG and a simplified action glyph SVG are rasterised at build time into PNGs at each required size, written to the package and referenced from the manifest.logo.svgfull marksharpdensity 38416 · 32 · 48 · 96 · 128icons/*.pnga simpler glyph for the toolbaraction-glyph.svgthick strokessharp16 and 32action-16/32.pngdefault_icon
Two sources — the full mark and a 16-pixel glyph — cover every surface.

3. Change the toolbar icon at runtime with a size map

 1// sw.js
 2export async function setStateIcon(state, tabId) {
 3  await chrome.action.setIcon({
 4    tabId,
 5    path: {
 6      16: `icons/action-${state}-16.png`,
 7      32: `icons/action-${state}-32.png`,
 8    },
 9  });
10}

Execution context: the service worker. Passing a single path instead of a size map makes the browser scale one image for both densities, reintroducing the blur. setIcon also accepts imageData for icons drawn at runtime with OffscreenCanvas, again keyed by size — see drawing dynamic action icons with OffscreenCanvas.

4. Design for light and dark toolbars

1// Choose an icon variant by the system colour scheme (from an extension page)
2const dark = matchMedia("(prefers-color-scheme: dark)").matches;
3chrome.runtime.sendMessage({ type: "theme", dark });

Execution context: an extension page such as the popup, which has matchMedia; the service worker does not. Chrome has no manifest key for theme-specific action icons, so either design one icon with enough contrast on both light and dark toolbars — a coloured mark with a subtle outline usually works — or swap icons at runtime based on the scheme an extension page reports. Firefox supports theme_icons in its action key for exactly this.

5. Provide store and Safari assets separately

The store listing needs assets the manifest does not carry: a 440×280 small promotional tile and screenshots at 1280×800 or 640×400 for the Chrome Web Store, and an app icon set (including 1024×1024) for the Safari containing app in Xcode’s asset catalogue.

1sharp-cli -i art/logo.svg -o store/icon-128.png resize 128 128
2sharp-cli -i art/promo.svg -o store/promo-440x280.png resize 440 280

Execution context: build or release scripts. Store assets are uploaded through each dashboard, not packaged. Keeping them generated from the same vector source keeps branding consistent. Listing assets are covered in store listing screenshots and promo images.

Pixel sizes an extension should shipThe PNG sizes recommended for a Chrome extension package: 16, 32, 48, 96 and 128 pixels.16 (toolbar 1×, menus)16 px32 (toolbar 2×)32 px48 (extensions page)48 px96 (extensions page 2×)96 px128 (install, store)128 px
Five rasters in the package; promotional assets live in the store dashboard.

6. Check the icons in CI

1// scripts/check-icons.mjs
2import sharp from "sharp";
3import fs from "node:fs";
4const m = JSON.parse(fs.readFileSync("dist/manifest.json", "utf8"));
5const maps = [m.icons, m.action?.default_icon].filter(Boolean);
6for (const map of maps) for (const [size, file] of Object.entries(map)) {
7  const { width, height } = await sharp(`dist/${file}`).metadata();
8  if (width !== Number(size) || height !== Number(size)) throw new Error(`${file} is ${width}x${height}, declared ${size}`);
9}

Execution context: Node in CI. A file declared as 32 pixels that is actually 128 still “works”, but the browser downscales it and the result is blurry — exactly the defect this guide is about. The check also catches missing files, which otherwise surface only as a broken image on the extensions page.

Common mistakes

  • One icon for everything. A 128-pixel logo scaled to 16 is mush. Ship each size, and a simplified glyph for the toolbar.
  • SVG in the manifest. Chrome rejects SVG for icons and action.default_icon. Rasterise at build time.
  • A single path in setIcon. Pass a size map so high-density displays get the 32-pixel image.
  • Transparent icons with no contrast. A dark glyph on a transparent background disappears on a dark toolbar. Test on both themes.
  • Forgetting the action icon entirely. Without action.default_icon, Chrome falls back to icons, which may be fine — or may render a detailed logo illegibly at 16 pixels.

Cross-browser variation

  • Chrome / Edge: PNG sizes as above; no theme-aware action icons in the manifest.
  • Firefox: accepts SVG icons, which scale perfectly, and supports theme_icons under action to provide light and dark variants. Keep PNGs in the shared manifest and add SVG in a Firefox-specific build if you want the sharper result.
  • Safari: uses the manifest icons for the toolbar and the containing app’s asset catalogue for the app itself; toolbar icons are often rendered as monochrome templates, so a single-colour glyph with transparency works best.

Verification

  1. Load the extension and view the toolbar icon at 100% and 200% display scaling: both should be crisp.
  2. Open chrome://extensions and the install dialog (pack and drag in the .crx) and confirm the 48- and 128-pixel icons are sharp.
  3. Switch the OS between light and dark mode and confirm the toolbar icon remains visible.
  4. Run a build check that every path in icons and action.default_icon exists in dist.

FAQ

Does the toolbar ever use sizes other than 16 and 32?

At unusual zoom levels or display scales Chrome may pick 48 or larger and scale down. Supplying 16, 32 and the identity sizes covers it.

Should the toolbar icon include text?

No. At 16 pixels, text is illegible. Use the badge for short text and keep the icon a symbol.

Can I animate the toolbar icon?

Only by calling setIcon repeatedly, which wakes the worker and costs CPU. Prefer a static state change or the badge.

Other MV3 Architecture & Extension Lifecycle Resources