Build a Favicon Changer Chrome Extension: Customize Tab Icons
The humble favicon—the small icon displayed in your browser tab—does more than just look pretty. It serves as a visual anchor, helping users quickly identify and distinguish between dozens of open tabs. While Chrome assigns favicons automatically based on websites, there are compelling reasons to build a favicon changer Chrome extension: branding consistency, personalization, productivity enhancements, and even accessibility improvements.
In this comprehensive tutorial, you’ll learn how to build a fully functional favicon changer extension using Manifest V3. Whether you want to create a fun personalization tool or a serious productivity application, this guide walks you through every step of the development process.
Understanding How Favicons Work in Chrome
Before diving into code, it’s essential to understand how Chrome handles favicons and what APIs are available to developers.
The Traditional Favicon System
Traditionally, websites specify their favicon using a link tag in the HTML head:
<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png" href="/favicon-32x32.png">
Chrome automatically fetches these icons from websites and caches them locally. The browser uses the Chrome Favicon Service to retrieve these icons efficiently across all tabs.
The Chrome Favicon API
Chrome provides a powerful Favicon API that extension developers can leverage:
chrome.favicon.getFavicon(url, callback)
However, this API is primarily for reading existing favicons. For changing favicons dynamically, we need to use a different approach involving the Tab API and declarativeNetRequest or direct DOM manipulation through content scripts.
Key Challenge: Manifest V3 Restrictions
In Manifest V3, extensions have limited ability to modify page content directly. The approach we used in Manifest V2—injecting scripts to change favicons—is restricted. Instead, we’ll explore legitimate methods that work within Chrome’s security model:
- Override Pages: Replace the new tab page with a custom one that shows your chosen icons
- Tab Updates: Use the
chrome.tabs.onUpdatedevent to detect page loads - Content Scripts: Inject scripts that modify favicons on specific domains (with user permission)
- Action Icons: Change the extension’s own action icon dynamically
Project Setup and Directory Structure
Let’s start by setting up our project. Create a new folder for your extension and set up the following structure:
favicon-changer/
├── manifest.json
├── popup.html
├── popup.js
├── popup.css
├── background.js
├── content.js
├── icons/
│ ├── icon16.png
│ ├── icon32.png
│ ├── icon48.png
│ └── icon128.png
└── favicons/
├── custom1.png
├── custom2.png
└── custom3.png
Creating the Manifest (Manifest V3)
The manifest.json file is the heart of every Chrome extension. Here’s our configuration for a favicon changer extension:
{
"manifest_version": 3,
"name": "Favicon Changer - Custom Tab Icons",
"version": "1.0.0",
"description": "Change favicons for any website with custom icons. Personalize your browsing experience.",
"permissions": [
"activeTab",
"storage",
"tabs"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
Key Manifest Components Explained
- permissions: We request
activeTabfor accessing the current tab,storagefor saving user preferences, andtabsfor tab information. - host_permissions:
<all_urls>allows our extension to work on any website. - action: Defines our popup UI and default icons.
- background: Registers a service worker for handling events.
- content_scripts: Injects our script into web pages to modify favicons.
Building the Popup Interface
The popup is what users see when they click our extension icon. Let’s create an intuitive interface for selecting custom favicons:
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Favicon Changer</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<header>
<h1>Favicon Changer</h1>
<p class="subtitle">Customize your tab icons</p>
</header>
<section class="current-site">
<h2>Current Site</h2>
<div id="current-site-info" class="site-info">
<span class="loading">Detecting current tab...</span>
</div>
</section>
<section class="favicon-selection">
<h2>Choose Favicon</h2>
<div class="favicon-grid" id="favicon-grid">
<!-- Favicon options will be populated by JavaScript -->
</div>
</section>
<section class="custom-upload">
<h2>Upload Custom Icon</h2>
<input type="file" id="custom-icon-input" accept="image/*">
<button id="upload-btn" class="btn-primary">Upload</button>
</section>
<section class="actions">
<button id="apply-btn" class="btn-primary">Apply to Current Tab</button>
<button id="reset-btn" class="btn-secondary">Reset to Original</button>
</section>
<section class="saved-favicons">
<h2>Saved Favicons</h2>
<div id="saved-list" class="saved-list">
<p class="empty-state">No saved favicons yet</p>
</div>
</section>
</div>
<script src="popup.js"></script>
</body>
</html>
popup.css
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
width: 320px;
background-color: #f8f9fa;
color: #333;
}
.container {
padding: 16px;
}
header {
text-align: center;
margin-bottom: 20px;
border-bottom: 1px solid #e0e0e0;
padding-bottom: 12px;
}
h1 {
font-size: 18px;
font-weight: 600;
color: #1a73e8;
}
h2 {
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
color: #555;
}
.subtitle {
font-size: 12px;
color: #777;
margin-top: 4px;
}
section {
margin-bottom: 20px;
}
.site-info {
background: white;
padding: 12px;
border-radius: 8px;
border: 1px solid #e0e0e0;
font-size: 13px;
}
.site-info .site-url {
font-weight: 500;
color: #1a73e8;
}
.favicon-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
background: white;
padding: 12px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.favicon-option {
width: 48px;
height: 48px;
border-radius: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
}
.favicon-option:hover {
border-color: #1a73e8;
transform: scale(1.05);
}
.favicon-option.selected {
border-color: #1a73e8;
background: #e8f0fe;
}
.favicon-option img {
width: 32px;
height: 32px;
}
.custom-upload {
display: flex;
flex-direction: column;
gap: 8px;
}
.custom-upload input[type="file"] {
font-size: 12px;
}
.btn-primary, .btn-secondary {
width: 100%;
padding: 10px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease;
}
.btn-primary {
background: #1a73e8;
color: white;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-secondary {
background: #e8eaed;
color: #333;
margin-top: 8px;
}
.btn-secondary:hover {
background: #d3d3d3;
}
.saved-list {
background: white;
border-radius: 8px;
border: 1px solid #e0e0e0;
padding: 8px;
max-height: 120px;
overflow-y: auto;
}
.empty-state {
text-align: center;
color: #999;
font-size: 12px;
padding: 20px;
}
.saved-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 4px;
margin-bottom: 4px;
}
.saved-item:hover {
background: #f5f5f5;
}
.saved-item .site-domain {
font-size: 12px;
color: #555;
}
.saved-item .remove-btn {
background: none;
border: none;
color: #ea4335;
cursor: pointer;
font-size: 16px;
}
Implementing the Popup Logic
Now let’s create the JavaScript that powers our popup:
popup.js
// Default favicon options
const defaultFavicons = [
{ id: 'star', name: 'Star', color: '#FFD700', icon: '⭐' },
{ id: 'heart', name: 'Heart', color: '#FF6B6B', icon: '❤️' },
{ id: 'fire', name: 'Fire', color: '#FF4500', icon: '🔥' },
{ id: 'check', name: 'Check', color: '#4CAF50', icon: '✅' },
{ id: 'warning', name: 'Warning', color: '#FF9800', icon: '⚠️' },
{ id: 'info', name: 'Info', color: '#2196F3', icon: 'ℹ️' },
{ id: 'rocket', name: 'Rocket', color: '#9C27B0', icon: '🚀' },
{ id: 'target', name: 'Target', color: '#E91E63', icon: '🎯' }
];
let currentTab = null;
let selectedFavicon = null;
// Initialize the popup
document.addEventListener('DOMContentLoaded', async () => {
await initPopup();
setupEventListeners();
});
async function initPopup() {
// Get the current active tab
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
currentTab = tabs[0];
if (currentTab) {
displayCurrentSite(currentTab);
loadSavedFavicons();
await checkCurrentFavicon(currentTab.id);
}
// Populate favicon grid
populateFaviconGrid();
}
function displayCurrentSite(tab) {
const infoDiv = document.getElementById('current-site-info');
const url = new URL(tab.url);
infoDiv.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<img src="${tab.favIconUrl || 'icons/icon16.png'}" style="width: 16px; height: 16px;">
<span class="site-url">${url.hostname}</span>
</div>
`;
}
function populateFaviconGrid() {
const grid = document.getElementById('favicon-grid');
defaultFavicons.forEach(favicon => {
const option = document.createElement('div');
option.className = 'favicon-option';
option.dataset.id = favicon.id;
option.innerHTML = `
<img src="data:image/svg+xml,${encodeURIComponent(createColoredIcon(favicon.icon, favicon.color))}"
alt="${favicon.name}"
title="${favicon.name}">
`;
option.addEventListener('click', () => selectFavicon(favicon, option));
grid.appendChild(option);
});
}
function createColoredIcon(emoji, color) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="${color}"/>
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle" font-size="18">${emoji}</text>
</svg>`;
}
function selectFavicon(favicon, element) {
// Remove previous selection
document.querySelectorAll('.favicon-option').forEach(opt => {
opt.classList.remove('selected');
});
// Add selection to current
element.classList.add('selected');
selectedFavicon = favicon;
}
async function checkCurrentFavicon(tabId) {
try {
// Get any existing favicon settings from storage
const result = await chrome.storage.local.get(`favicon_${tabId}`);
if (result[`favicon_${tabId}`]) {
// Highlight the saved favicon
const savedFavicon = result[`favicon_${tabId}`];
const option = document.querySelector(`[data-id="${savedFavicon.id}"]`);
if (option) {
option.classList.add('selected');
selectedFavicon = savedFavicon;
}
}
} catch (error) {
console.error('Error checking current favicon:', error);
}
}
function setupEventListeners() {
// Apply button
document.getElementById('apply-btn').addEventListener('click', applyFavicon);
// Reset button
document.getElementById('reset-btn').addEventListener('click', resetFavicon);
// Upload button
document.getElementById('upload-btn').addEventListener('click', handleUpload);
}
async function applyFavicon() {
if (!currentTab || !selectedFavicon) {
alert('Please select a favicon first');
return;
}
try {
// Send message to content script to change favicon
await chrome.tabs.sendMessage(currentTab.id, {
action: 'changeFavicon',
favicon: selectedFavicon
});
// Save to storage
await chrome.storage.local.set({
[`favicon_${currentTab.id}`]: selectedFavicon
});
// Also save by domain for persistence
const url = new URL(currentTab.url);
const domain = url.hostname;
const savedFavicons = await chrome.storage.local.get('savedFavicons');
const saved = savedFavicons.savedFavicons || {};
saved[domain] = selectedFavicon;
await chrome.storage.local.set({ savedFavicons: saved });
loadSavedFavicons();
alert(`Favicon applied to ${domain}!`);
} catch (error) {
console.error('Error applying favicon:', error);
alert('Failed to apply favicon. Make sure you are on a web page.');
}
}
async function resetFavicon() {
if (!currentTab) return;
try {
await chrome.tabs.sendMessage(currentTab.id, {
action: 'resetFavicon'
});
// Remove from storage
await chrome.storage.local.remove(`favicon_${currentTab.id}`);
// Also remove from saved
const url = new URL(currentTab.url);
const domain = url.hostname;
const savedFavicons = await chrome.storage.local.get('savedFavicons');
const saved = savedFavicons.savedFavicons || {};
delete saved[domain];
await chrome.storage.local.set({ savedFavicons: saved });
// Remove selection
document.querySelectorAll('.favicon-option').forEach(opt => {
opt.classList.remove('selected');
});
selectedFavicon = null;
loadSavedFavicons();
alert('Favicon reset to original!');
} catch (error) {
console.error('Error resetting favicon:', error);
alert('Failed to reset favicon.');
}
}
async function handleUpload() {
const input = document.getElementById('custom-icon-input');
const file = input.files[0];
if (!file) {
alert('Please select an image file first');
return;
}
const reader = new FileReader();
reader.onload = async (e) => {
const customFavicon = {
id: 'custom_' + Date.now(),
name: file.name,
type: 'custom',
dataUrl: e.target.result
};
// Save custom favicon
const customFavicons = await chrome.storage.local.get('customFavicons');
const custom = customFavicons.customFavicons || [];
custom.push(customFavicon);
await chrome.storage.local.set({ customFavicons: custom });
// Apply to current tab
if (currentTab) {
await chrome.tabs.sendMessage(currentTab.id, {
action: 'changeFavicon',
favicon: customFavicon
});
}
alert('Custom favicon uploaded and applied!');
};
reader.readAsDataURL(file);
}
async function loadSavedFavicons() {
const savedList = document.getElementById('saved-list');
try {
const result = await chrome.storage.local.get('savedFavicons');
const saved = result.savedFavicons || {};
const entries = Object.entries(saved);
if (entries.length === 0) {
savedList.innerHTML = '<p class="empty-state">No saved favicons yet</p>';
return;
}
savedList.innerHTML = '';
entries.forEach(([domain, favicon]) => {
const item = document.createElement('div');
item.className = 'saved-item';
let iconSrc;
if (favicon.type === 'custom') {
iconSrc = favicon.dataUrl;
} else {
iconSrc = `data:image/svg+xml,${encodeURIComponent(createColoredIcon(favicon.icon, favicon.color))}`;
}
item.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px;">
<img src="${iconSrc}" style="width: 20px; height: 20px;">
<span class="site-domain">${domain}</span>
</div>
<button class="remove-btn" data-domain="${domain}">×</button>
`;
item.querySelector('.remove-btn').addEventListener('click', async (e) => {
const domainToRemove = e.target.dataset.domain;
delete saved[domainToRemove];
await chrome.storage.local.set({ savedFavicons: saved });
loadSavedFavicons();
});
savedList.appendChild(item);
});
} catch (error) {
console.error('Error loading saved favicons:', error);
}
}
Creating the Content Script
The content script runs in the context of web pages and handles the actual favicon modification:
content.js
// Content script for modifying favicons
// Store the original favicon
let originalFavicon = null;
// Listen for messages from the popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'changeFavicon') {
changeFavicon(message.favicon);
sendResponse({ success: true });
} else if (message.action === 'resetFavicon') {
resetFavicon();
sendResponse({ success: true });
} else if (message.action === 'getCurrentFavicon') {
sendResponse({ favicon: currentCustomFavicon });
}
return true;
});
// Store the current custom favicon
let currentCustomFavicon = null;
function changeFavicon(favicon) {
// Store original favicon if not already stored
if (!originalFavicon) {
const link = document.querySelector("link[rel~='icon']") ||
document.querySelector("link[rel='shortcut icon']");
originalFavicon = link ? link.href : null;
}
// Remove any existing custom favicon links
removeCustomFaviconLinks();
// Create new favicon link
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png';
if (favicon.type === 'custom') {
// Custom uploaded image
link.href = favicon.dataUrl;
} else {
// Generate SVG favicon from emoji
link.href = createSvgFavicon(favicon.icon, favicon.color);
}
// Add unique ID to identify our custom links
link.id = 'custom-favicon-link';
document.head.appendChild(link);
// Also update the shortcut icon
const shortcutLink = document.createElement('link');
shortcutLink.rel = 'shortcut icon';
shortcutLink.href = link.href;
shortcutLink.id = 'custom-shortcut-favicon';
document.head.appendChild(shortcutLink);
// Store current favicon
currentCustomFavicon = favicon;
// Update any favicon metatags dynamically
updateDynamicFaviconMeta(link.href);
console.log('Favicon changed to:', favicon.name);
}
function createSvgFavicon(emoji, color) {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<rect width="32" height="32" rx="4" fill="${color}"/>
<text x="50%" y="50%" dominant-baseline="central" text-anchor="middle"
font-size="18" font-family="Arial">${emoji}</text>
</svg>
`;
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
}
function removeCustomFaviconLinks() {
const customLinks = document.querySelectorAll('#custom-favicon-link, #custom-shortcut-favicon');
customLinks.forEach(link => link.remove());
}
function resetFavicon() {
// Remove custom favicon links
removeCustomFaviconLinks();
// If we have an original, we'd need to reload the page or restore it
// For now, we'll just clear the currentCustomFavicon
currentCustomFavicon = null;
// Note: Truly restoring the original favicon requires reloading the page
// or the browser's built-in favicon handling to kick in
console.log('Favicon reset requested');
}
function updateDynamicFaviconMeta(href) {
// Try to update any og:image or other meta tags that might reference favicon
const existingMeta = document.querySelector('meta[property="og:image"]');
if (existingMeta) {
// Some sites use og:image as favicon fallback
// We won't modify this as it affects social sharing
}
}
// Handle page navigation (for SPAs)
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
// Page changed - custom favicon will be lost
// This is a limitation of content scripts in Manifest V3
// Users need to re-apply favicon on navigation
currentCustomFavicon = null;
}
}).observe(document, { subtree: true, childList: true });
Background Service Worker
The service worker handles extension lifecycle and events:
background.js
// Background service worker for Favicon Changer extension
// Handle extension installation
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
console.log('Favicon Changer extension installed');
// Set default values
chrome.storage.local.set({
savedFavicons: {},
customFavicons: []
});
}
});
// Handle tab updates - reapply favicon for saved domains
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
try {
const url = new URL(tab.url);
const domain = url.hostname;
// Check if we have a saved favicon for this domain
const result = await chrome.storage.local.get('savedFavicons');
const savedFavicons = result.savedFavicons || {};
if (savedFavicons[domain]) {
// Send message to apply saved favicon
chrome.tabs.sendMessage(tabId, {
action: 'changeFavicon',
favicon: savedFavicons[domain]
}).catch(() => {
// Content script might not be loaded yet
// This is fine - user can manually apply
});
}
} catch (error) {
// Ignore errors for invalid URLs
}
}
});
// Handle messages from content scripts or popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getSavedFavicon') {
chrome.storage.local.get('savedFavicons').then(result => {
sendResponse(result.savedFavicons || {});
});
return true;
}
});
Testing Your Extension
Now that we’ve built all the components, let’s test our extension:
Loading the Extension in Chrome
- Open Chrome and navigate to
chrome://extensions/ - Enable Developer mode using the toggle in the top right
- Click Load unpacked
- Select your
favicon-changerfolder - The extension should now appear in your toolbar
Testing the Functionality
- Navigate to any website (e.g., github.com)
- Click the Favicon Changer extension icon
- The popup should show the current site
- Click on any favicon option
- Click “Apply to Current Tab”
- The favicon should change to your selected icon
- Refresh the page - the favicon should persist (if saved)
- Try the “Reset to Original” button
Debugging Tips
- Popup not opening? Check the console in
chrome://extensions/ - Favicon not changing? Make sure content scripts are injected correctly
- Changes not persisting? Check the storage in
chrome://extensions/> Service Worker console - Error messages? Use
chrome.runtime.lastErrorto catch async errors
Publishing to the Chrome Web Store
Once your extension is working, you can publish it:
Prepare for Publishing
- Create a ZIP file of your extension folder (excluding source files if needed)
- Take screenshots of your extension in action (minimum 1280x800, maximum 3840x2160)
- Write a detailed description explaining features and functionality
- Set a clear privacy practice disclosure
Submit to Chrome Web Store
- Go to the Chrome Web Store Developer Dashboard
- Click New Item and upload your ZIP file
- Fill in all required fields:
- Store listing (name, description, screenshots)
- Privacy practice
- Category
- Submit for review
Review Process
Google reviews extensions for:
- Malicious behavior
- Misleading functionality
- Privacy violations
- Trademark issues
Review typically takes 1-3 days. Once approved, your extension will be available to all Chrome users.
Advanced Features and Improvements
Here are some ideas to enhance your favicon changer extension:
1. Bulk Operations
- Apply the same favicon to all tabs from a domain
- Queue multiple favicon changes
2. Preset Themes
- Create themed icon packs (gaming, business, nature)
- Allow users to import/export themes
3. Dynamic Favicons Based on Page Content
- Change favicon based on page title keywords
- Show notification badges on favicon
4. Sync Across Devices
- Use Chrome Sync storage to save preferences
- Sync across Chrome profiles
5. Integration with Other Extensions
- Combine with tab management extensions
- Add keyboard shortcuts for quick changes
Troubleshooting Common Issues
Issue: Favicon Changes Don’t Persist on Navigation
Cause: Single-page applications (SPAs) don’t trigger full page loads.
Solution: Use the History API to detect URL changes, or implement a “sticky” mode that re-applies favicons automatically.
Issue: Extension Doesn’t Work on Some Sites
Cause: Some sites use aggressive CSP (Content Security Policy) or frame-breaking techniques.
Solution: Test across different sites and document compatibility. Some sites may require workarounds.
Issue: Custom Icons Look Pixelated
Cause: Using small images or inappropriate formats.
Solution: Use PNG icons at 16x16, 32x32, and 128x128 pixels. Test on high-DPI displays.
Conclusion
Congratulations! You’ve built a complete favicon changer Chrome extension from scratch. This extension demonstrates several key concepts in Chrome extension development:
- Manifest V3 architecture with service workers
- Content scripts for page interaction
- Storage API for persisting user preferences
- Message passing between popup, background, and content scripts
- Dynamic favicon generation using SVG data URIs
The favicon changer is a practical tool that users genuinely appreciate. You can now extend this foundation to add more features, polish the UI, or publish it to the Chrome Web Store.
Remember to test thoroughly across different browsers and websites, and always follow Chrome’s policies when publishing. Happy coding!
Additional Resources
- Chrome Extension Documentation
- Manifest V3 Migration Guide
- Chrome Web Store Publishing
- Extension Samples
Start building your own Chrome extensions today and join the millions of developers creating powerful browser extensions that millions of users enjoy!