Back
Lesson 7

Changing Icons Programmatically

View Source Code

Overview

Add visual feedback by changing the extension icon dynamically based on whether the current page has notes. This lesson covers extension metadata configuration, icon specifications, and using the Action API to update icons programmatically.

By the end, TabNotes will display different icons for pages with and without notes, providing instant visual status.

What You’ll Learn

  • Configure extension metadata (name, version, description)
  • Understand extension icon size requirements
  • Generate multi-size icon sets
  • Use browser.action.setIcon() for dynamic icon changes
  • Implement per-tab icon states
  • Create reusable utility functions for icon management

Configuring Extension Metadata

Before tackling dynamic icons, set proper extension metadata in wxt.config.ts:

// wxt.config.ts
import { defineConfig } from 'wxt';

export default defineConfig({
  manifest: {
    name: 'TabNotes',
    version: '1.0.0',
    description: 'Add notes to any tab in your browser',
    permissions: ['storage', 'tabs', 'scripting']
  }
});

Restart Required: Manifest changes require dev server restart (Ctrl+C then bun dev).

Where This Appears:

  • chrome://extensions page (extension card)
  • Chrome Web Store listing (when published)
  • Extension popup title bar

Understanding Extension Icons

Extensions require icons in multiple sizes for different contexts:

SizeUsage
16×16Extension toolbar, browser bar
32×32Retina display (2× scaling for 16px)
48×48Extensions management page
96×96Retina display (2× scaling for 48px)
128×128Chrome Web Store, installation prompt

File Format: PNG (supports transparency)

Icon Storage Location

Place icons in public/icon/ directory:

public/
└── icon/
    ├── 16.png
    ├── 32.png
    ├── 48.png
    ├── 96.png
    └── 128.png

Why public/? WXT copies public/ contents directly to the extension bundle without processing. Perfect for static assets like icons.


Generating Multi-Size Icons

Option 1: Manual Export

Use any image editor (Photoshop, Figma, etc.) to export your design at each required size.

Option 2: Automated Generation

Use favicomatic.com to generate all sizes from a single image:

  1. Upload your icon design (SVG or high-res PNG)
  2. Select required sizes: 16, 32, 48, 96, 128
  3. Download generated icons
  4. Note: Tool sometimes generates .ico files - convert to PNG if needed

Creating Two Icon States

For TabNotes, create two icon sets:

Empty State (public/icon/)

  • Outline-style icon
  • Indicates no notes on current page

Full State (public/icon-full/)

  • Filled/solid-style icon
  • Indicates page has saved notes
Empty icon state - outline styleEmpty State
Full icon state - filled styleFull State

Design Tip: Icons should be visually similar with clear differentiation (outline vs filled, color change, badge, etc.)


Implementing Dynamic Icons

Create a utility function to change icons based on note existence:

Step 1: Create Utility Function

// utils/changeIcon.ts
import { storage } from 'wxt/storage';
import getPageKey from './getPageKey';

const changeIcon = async (tabId: number) => {
  // Get tab details
  const tab = await browser.tabs.get(tabId);
  if (!tab.url) return;
  
  // Check if notes exist for this page
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage');
  const noteData = notesStorage?.[pageKey];
  
  if (noteData && noteData.note) {
    // Has notes - show full icon
    await browser.action.setIcon({
      tabId,
      path: {
        "16": "/icon-full/16.png",
        "32": "/icon-full/32.png",
        "48": "/icon-full/48.png",
        "96": "/icon-full/96.png",
        "128": "/icon-full/128.png"
      }
    });
  } else {
    // No notes - show empty icon
    await browser.action.setIcon({
      tabId,
      path: {
        "16": "/icon/16.png",
        "32": "/icon/32.png",
        "48": "/icon/48.png",
        "96": "/icon/96.png",
        "128": "/icon/128.png"
      }
    });
  }
};

export default changeIcon;

Key API: browser.action.setIcon()

  • tabId - Specific tab to update (icon changes per-tab, not globally)
  • path - Object mapping sizes to file paths

Why All Sizes? Chrome selects the appropriate size based on display context and pixel density.

Step 2: Call from Popup

Update the icon when users save notes:

// entrypoints/popup/main.ts
import { storage } from 'wxt/storage';
import changeIcon from '@/utils/changeIcon';

// ... existing popup code ...

noteText.addEventListener('keyup', async () => {
  const [tab] = await browser.tabs.query({ 
    active: true, 
    currentWindow: true 
  });
  
  if (!tab.id || !tab.url) return;
  
  const pageKey = getPageKey(tab.url);
  const notesStorage = await storage.getItem('sync:notesStorage') || {};
  
  // Update note
  const existingNote = notesStorage[pageKey];
  notesStorage[pageKey] = {
    note: noteText.value,
    createdAt: existingNote?.createdAt || new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  
  await storage.setItem('sync:notesStorage', notesStorage);
  
  // Update icon to reflect new state
  await changeIcon(tab.id);
});

Testing Dynamic Icons

  1. Open popup on any page, type a note
  2. Icon changes to “full” state immediately
  3. Delete all note text (clear textarea)
  4. Icon changes back to “empty” state

Current Limitation: Icon only updates when typing in popup. It doesn’t update when switching tabs. We’ll fix this in Lesson 8.


Per-Tab Icon States

browser.action.setIcon() accepts a tabId parameter, making icons tab-specific:

// Different icons for different tabs
await browser.action.setIcon({
  tabId: 123,  // Tab 123 shows full icon
  path: { "16": "/icon-full/16.png", ... }
});

await browser.action.setIcon({
  tabId: 456,  // Tab 456 shows empty icon
  path: { "16": "/icon/16.png", ... }
});

When users switch between tabs, Chrome displays the correct icon automatically.

Alternative: Global icon change (affects all tabs):

await browser.action.setIcon({
  // No tabId - applies to all tabs
  path: { "16": "/icon-full/16.png", ... }
});

For TabNotes, per-tab icons make sense - each page has its own note state.


Icon Path Best Practices

Absolute Paths

path: {
  "16": "/icon/16.png"  // ✅ Starts with / (absolute)
}

Paths are relative to extension root. Leading / ensures correct resolution.

All Sizes Required

While Chrome can scale icons, provide all sizes for best quality:

// ❌ Missing sizes
path: {
  "16": "/icon/16.png"
}

// ✅ Complete set
path: {
  "16": "/icon/16.png",
  "32": "/icon/32.png",
  "48": "/icon/48.png",
  "96": "/icon/96.png",
  "128": "/icon/128.png"
}

Icon Design Tips

Visual Clarity:

  • Icons should be recognizable at 16×16px
  • Avoid fine details that disappear when small
  • Use high contrast

State Differentiation:

  • Make empty vs full states obviously different
  • Common patterns: outline vs filled, monochrome vs color, badge indicators

Consistency:

  • Match your extension’s visual brand
  • Align with Chrome’s design language
  • Test on light and dark backgrounds

Common Issues

Icon Files Not Found (404 Error)

Cause: Icons not in public/ directory

Fix: Move icons to public/icon/ and restart dev server

ICO Files Instead of PNG

Some icon generators export .ico format. Chrome extensions require PNG.

Fix: Convert using online tools or image editors

Extension Breaks on Icon Change

If dev server crashes when changing icons:

  1. Press Ctrl+C to stop
  2. Run bun dev to restart
  3. Changes to public/ sometimes require restart

What’s Next

Icons now change when users type notes, providing instant visual feedback. However, the icon doesn’t update when switching tabs - it only reflects the current tab’s state after typing.

Lesson 8 implements tab change detection using background scripts, ensuring icons automatically update when users navigate between pages with and without notes.