I once lost twenty minutes debugging a “broken” feature.
Turns out… I was testing the production build.
Same icon. Same name. Different environment. Completely my fault.
If you build Chrome extensions long enough, this will happen to you. It’s basically a rite of passage. But it’s also avoidable.
If you’re using a modern framework like Plasmo or Wxt, you’re already halfway there. Here’s how I make sure I never mix up dev and prod again.
The Smart Framework Way with Plasmo
By default, Plasmo turns your development icon grayscale. That tiny visual cue saves you instantly.
No thinking. No guessing.
If you want more control, drop an icon.development.png into your assets/ folder.
assets/
icon.png
icon.development.pngPlasmo swaps it automatically during development builds.
Zero manual effort. Zero confusion.
Another simpler Way with Wxt
Wxt doesn’t force grayscale by default, but it gives you better control.
Icons are auto-detected from the public/ directory. You just place files there and Wxt wires them into the manifest.
Basic structure:
public/
icon-16.png
icon-48.png
icon-128.pngWxt scans this directory and injects them into the manifest automatically.
Option 1: Use the auto-icons module (cleanest)
Install the official module:
npm i -D @wxt-dev/auto-iconsThen enable it:
// wxt.config.ts
export default defineConfig({
modules: ['@wxt-dev/auto-icons'],
})Drop a single base icon src/assets/icon.png
Now here’s the useful part: the module can apply grayscale or overlays during development.
That means your dev build can look visually different without you manually swapping files.
You get automatic icon sizing plus a dev-only visual indicator.
Option 2: Environment-based icon swap
Wxt supports env modes (import.meta.env.MODE, DEV, PROD).
So you can change icons at build time.
Example:
public/
icons/
dev-48.png
prod-48.pngThen in your config:
// wxt.config.ts
export default defineConfig({
manifest: () => {
const isDev = process.env.NODE_ENV !== 'production'
return {
icons: {
48: isDev ? '/icons/dev-48.png' : '/icons/prod-48.png',
128: isDev ? '/icons/dev-128.png' : '/icons/prod-128.png',
},
}
},
})
Now, dev and prod builds compile with different icons automatically.
No manual switching.
If you’re using CRXJS, you can dynamically override the icons field in your config using extendManifest based on your environment.
Let the tooling do the work.
The Build Script Swap (No Framework)
Not using a framework?
No problem.
You probably have a dist folder anyway. Something like this
src/
icons/
icon-dev.png
icon-prod.png
dist/Here’s what I do:
- Keep
icon-dev.pngin source. - Keep
icon-prod.pngin source. - During production build, copy
icon-prod.png→ rename toicon.png→ place insidedist.
Add a build step:
{
"scripts": {
"build": "node scripts/swap-icon.js && vite build"
}
}Script:
// scripts/swap-icon.js
import fs from "fs"
const isProd = process.env.NODE_ENV === "production"
const src = isProd
? "src/icons/icon-prod.png"
: "src/icons/icon-dev.png"
fs.copyFileSync(src, "public/icon.png")
That’s it. One command. Always correct icon.
Your source can stay a bit messy. But your production build stays clean.
And most importantly, your toolbar tells you the truth.
Simple automation beats human memory every time.
Dynamic Icon Swapping (If You Want to Be Fancy)
If you like runtime control, you can use the chrome.action.setIcon() API in Manifest V3.
Inside your background script:
- Check the environment.
- Look at the version string.
- Or use a custom flag.
Then call:
chrome.action.setIcon({ path: "dev-icon.png" })
This changes the toolbar icon dynamically.
Important detail:
It won’t change the icon on chrome://extensions.
But in the toolbar, where you actually look it works perfectly.
Save Your Sanity
Here’s the thing.
You only need to get burned once before this matters.
A different icon is a tiny change. But it protects your focus. And focus is everything when you’re building.
Set it up once.
Make it obvious.
And never waste another debugging session clicking the wrong extension again.