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 state

Theme-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+" badge

Real-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

Related Articles

An unhandled error has occurred. Reload 🗙