Dynamic Favicon Guide
Master dynamic favicons: JavaScript updates, notification badges, theme switching, canvas generation, real-time changes, and interactive favicon techniques.
Dynamic Favicon Use Cases
Notifications
Unread count badge
Theme Switch
Dark/light mode
Status
Loading indicator
Live Data
Real-time updates
Basic Favicon Switching
JavaScript Favicon Update
Simple Favicon Change
// Function to change favicon
function changeFavicon(src) {
// Find existing favicon link
let link = document.querySelector("link[rel~='icon']");
if (!link) {
// Create if doesn't exist
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
// Update href
link.href = src;
}
// Usage examples:
changeFavicon('/favicon-dark.ico'); // Dark mode
changeFavicon('/favicon-light.ico'); // Light mode
changeFavicon('/favicon-notification.ico'); // Notification stateTheme-Based Switching
// Detect user's theme preference
function updateFaviconForTheme() {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const faviconPath = isDark ? '/favicon-dark.png' : '/favicon-light.png';
changeFavicon(faviconPath);
}
// Run on page load
updateFaviconForTheme();
// Listen for theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
const faviconPath = e.matches ? '/favicon-dark.png' : '/favicon-light.png';
changeFavicon(faviconPath);
});React/Vue Component Example
// React Hook
import { useEffect } from 'react';
function useFavicon(faviconUrl) {
useEffect(() => {
const link = document.querySelector("link[rel~='icon']") ||
document.createElement('link');
link.rel = 'icon';
link.href = faviconUrl;
if (!document.querySelector("link[rel~='icon']")) {
document.head.appendChild(link);
}
return () => {
// Optional cleanup
};
}, [faviconUrl]);
}
// Usage in component
function App() {
const [theme, setTheme] = useState('light');
useFavicon(`/favicon-${theme}.ico`);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}Notification Badge (Unread Count)
Canvas-Based Badge Generation
Complete Implementation
class FaviconBadge {
constructor(baseFaviconUrl) {
this.baseFaviconUrl = baseFaviconUrl;
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.size = 32; // Favicon size
this.canvas.width = this.size;
this.canvas.height = this.size;
}
// Draw badge with number
drawBadge(count) {
return new Promise((resolve) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
// Clear canvas
this.ctx.clearRect(0, 0, this.size, this.size);
// Draw base favicon
this.ctx.drawImage(img, 0, 0, this.size, this.size);
if (count > 0) {
// Badge background
this.ctx.fillStyle = '#FF0000';
this.ctx.beginPath();
this.ctx.arc(24, 8, 8, 0, 2 * Math.PI);
this.ctx.fill();
// Badge text
this.ctx.fillStyle = '#FFFFFF';
this.ctx.font = 'bold 10px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
const text = count > 99 ? '99+' : count.toString();
this.ctx.fillText(text, 24, 8);
}
resolve(this.canvas.toDataURL('image/png'));
};
img.src = this.baseFaviconUrl;
});
}
// Update favicon with badge
async updateFavicon(count) {
const dataUrl = await this.drawBadge(count);
let link = document.querySelector("link[rel~='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = dataUrl;
}
}
// Usage:
const faviconBadge = new FaviconBadge('/favicon-base.png');
// Show unread count
faviconBadge.updateFavicon(5); // Shows "5" badge
// Clear badge
faviconBadge.updateFavicon(0); // No badge
// Many notifications
faviconBadge.updateFavicon(150); // Shows "99+" badgeReal-World Example (Chat App)
// Initialize
const badge = new FaviconBadge('/favicon.png');
let unreadMessages = 0;
// Listen for new messages
socket.on('new_message', (message) => {
if (document.hidden) { // Only if tab not active
unreadMessages++;
badge.updateFavicon(unreadMessages);
// Optional: Update page title too
document.title = `(${unreadMessages}) Chat App`;
}
});
// Clear when tab becomes active
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
unreadMessages = 0;
badge.updateFavicon(0);
document.title = 'Chat App';
}
});Animated/Rotating Favicon
Loading Indicator & Animations
Frame-Based Animation
class AnimatedFavicon {
constructor(frames, fps = 10) {
this.frames = frames; // Array of favicon URLs
this.fps = fps;
this.currentFrame = 0;
this.interval = null;
}
start() {
if (this.interval) return; // Already running
this.interval = setInterval(() => {
const link = document.querySelector("link[rel~='icon']") ||
document.createElement('link');
link.rel = 'icon';
link.href = this.frames[this.currentFrame];
if (!document.querySelector("link[rel~='icon']")) {
document.head.appendChild(link);
}
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
}, 1000 / this.fps);
}
stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
this.currentFrame = 0;
}
}
}
// Usage: Loading spinner
const loadingFrames = [
'/loading-1.png',
'/loading-2.png',
'/loading-3.png',
'/loading-4.png'
];
const spinner = new AnimatedFavicon(loadingFrames, 4); // 4 FPS
// Start animation during API call
spinner.start();
fetch('/api/data')
.then(response => response.json())
.finally(() => {
spinner.stop();
changeFavicon('/favicon.png'); // Restore normal favicon
});Progress Bar Favicon
function drawProgressFavicon(percentage) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 32;
canvas.height = 32;
// Background
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, 32, 32);
// Progress bar background
ctx.fillStyle = '#ddd';
ctx.fillRect(4, 12, 24, 8);
// Progress bar fill
const fillWidth = (24 * percentage) / 100;
ctx.fillStyle = '#4CAF50';
ctx.fillRect(4, 12, fillWidth, 8);
// Update favicon
const link = document.querySelector("link[rel~='icon']") ||
document.createElement('link');
link.rel = 'icon';
link.href = canvas.toDataURL('image/png');
if (!document.querySelector("link[rel~='icon']")) {
document.head.appendChild(link);
}
}
// Usage: File upload progress
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
drawProgressFavicon(percentComplete);
}
});
xhr.addEventListener('load', () => {
changeFavicon('/favicon.png'); // Restore
});
xhr.open('POST', '/upload');
xhr.send(formData);
});Common Dynamic Favicon Patterns
Ready-to-Use Examples
// Change favicon when tab is inactive
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
changeFavicon('/favicon-inactive.png');
document.title = '?? Come back!';
} else {
changeFavicon('/favicon.png');
document.title = 'My App';
}
});// Show connection status
window.addEventListener('online', () => {
changeFavicon('/favicon-online.png');
console.log('Back online');
});
window.addEventListener('offline', () => {
changeFavicon('/favicon-offline.png');
console.log('Connection lost');
});function updateTimeBasedFavicon() {
const hour = new Date().getHours();
const isNight = hour >= 18 || hour < 6;
changeFavicon(isNight ? '/favicon-night.png' : '/favicon-day.png');
}
// Update immediately
updateTimeBasedFavicon();
// Check every hour (3600000 ms)
setInterval(updateTimeBasedFavicon, 3600000);// Show error favicon on API failure
fetch('/api/data')
.then(response => {
if (!response.ok) throw new Error('API error');
changeFavicon('/favicon.png');
return response.json();
})
.catch(error => {
changeFavicon('/favicon-error.png');
console.error('Error:', error);
});Dynamic Favicon Best Practices
? Best Practices
- Use Canvas API for dynamic badges/overlays
- Limit animation FPS to 2-4 frames/second
- Stop animations when tab is hidden
- Provide fallback static favicon
- Test across different browsers
- Use data URLs to avoid extra requests
- Consider accessibility (avoid flashing)
- Clean up intervals/listeners on unmount
? Common Mistakes
- Animating too fast (causes seizures)
- Not cleaning up intervals (memory leaks)
- Changing favicon too frequently
- Using large images (slow performance)
- Forgetting cross-origin canvas restrictions
- Not testing on Safari (limited support)
- Overusing dynamic favicons (distracting)
- Missing error handling in Canvas code
Browser Support
Compatibility Notes
| Browser | Dynamic Change | Canvas/Data URL | Notes |
|---|---|---|---|
| Chrome | ? Full | ? Full | Best support |
| Firefox | ? Full | ? Full | Works well |
| Safari | ? Limited | ? Limited | May not update immediately |
| Edge | ? Full | ? Full | Chromium-based works |
| Opera | ? Full | ? Full | Chromium-based works |
Generate Base Favicons for Dynamic Use
Create optimized favicon sets perfect for JavaScript manipulation
Generate Favicons