Animated & Dynamic Favicons Guide
Master animated favicons: JavaScript techniques, Canvas API, SVG animation, notification badges, and best practices for dynamic browser tab icons.
Why Animate Favicons?
Notifications
Alert users to updates
Status
Show loading/progress
Attention
Draw focus to tab
Activity
Real-time updates
Browser Support & Limitations
What Works & What Doesn't
| Browser | Canvas Animation | GIF Animation | SVG Animation | Notes |
|---|---|---|---|---|
| Chrome | ? Yes | ? No | ? No | Canvas-based only |
| Firefox | ? Yes | ? No | ? No | Static GIF frame only |
| Safari | ? Yes | ? No | ? No | Limited support |
| Edge | ? Yes | ? No | ? No | Same as Chrome |
Key Takeaway: Only Canvas-based JavaScript animation works reliably. GIF and SVG animations are NOT supported in favicons.
Canvas-Based Animation (Recommended)
JavaScript Canvas Technique
Basic Notification Badge Example:
// Simple notification badge
function updateFavicon(count) {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
// Load original favicon
const img = new Image();
img.onload = () => {
// Draw original icon
ctx.drawImage(img, 0, 0, 32, 32);
if (count > 0) {
// Draw notification badge
ctx.fillStyle = '#FF0000';
ctx.beginPath();
ctx.arc(24, 8, 8, 0, 2 * Math.PI);
ctx.fill();
// Draw count
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(count > 9 ? '9+' : count, 24, 8);
}
// Update favicon
const link = document.querySelector("link[rel*='icon']") ||
document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = canvas.toDataURL();
document.getElementsByTagName('head')[0].appendChild(link);
};
img.src = '/favicon.ico';
}
// Usage
updateFavicon(5); // Show badge with "5"
updateFavicon(0); // Remove badgeAnimated Loading Spinner:
let angle = 0;
function animateLoading() {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
// Clear canvas
ctx.clearRect(0, 0, 32, 32);
// Draw rotating arc
ctx.strokeStyle = '#0066CC';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(16, 16, 12, angle, angle + Math.PI * 1.5);
ctx.stroke();
angle += 0.1;
// Update favicon
const link = document.querySelector("link[rel*='icon']") ||
document.createElement('link');
link.href = canvas.toDataURL();
if (!link.parentNode) {
document.head.appendChild(link);
}
}
// Start animation
const intervalId = setInterval(animateLoading, 50);
// Stop animation
// clearInterval(intervalId);Progress Bar Favicon:
function showProgress(percentage) {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
// Background
ctx.fillStyle = '#E0E0E0';
ctx.fillRect(0, 0, 32, 32);
// Progress bar
ctx.fillStyle = '#4CAF50';
ctx.fillRect(0, 0, 32 * (percentage / 100), 32);
// Percentage text
ctx.fillStyle = '#000000';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(percentage + '%', 16, 16);
// Update favicon
const link = document.querySelector("link[rel*='icon']") ||
document.createElement('link');
link.href = canvas.toDataURL();
if (!link.parentNode) {
document.head.appendChild(link);
}
}
// Usage: showProgress(75);Favicon Animation Libraries
Favico.js
Features: Badges, animations, video
Size: ~7KB minified
Installation:<script src="favico.min.js"></script>const favicon = new Favico({
animation: 'slide'
});
favicon.badge(12); // Show badge
favicon.reset(); // Remove badgeTinycon
Features: Lightweight badges
Size: ~2KB minified
Installation:<script src="tinycon.min.js"></script>Tinycon.setOptions({
background: '#FF0000'
});
Tinycon.setBubble(7); // Show "7"
Tinycon.reset(); // ClearCommon Use Cases
Perfect for: Gmail, Slack, messaging apps
// Show unread count
let unreadCount = 0;
function updateUnreadCounter(count) {
unreadCount = count;
updateFavicon(count);
document.title = count > 0 ?
`(${count}) Messages` :
'Messages';
}
// WebSocket or polling update
socket.on('newMessage', () => {
updateUnreadCounter(++unreadCount);
});Perfect for: File uploads, cloud storage
// Track upload progress
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
showProgress(percent);
}
});
xhr.addEventListener('load', () => {
showProgress(100);
setTimeout(resetFavicon, 2000);
});Perfect for: Social media, alerts, dashboards
// Flash animation for attention
function flashFavicon() {
let isOriginal = true;
const flashInterval = setInterval(() => {
if (isOriginal) {
updateFavicon(1); // Show alert
} else {
resetFavicon(); // Show normal
}
isOriginal = !isOriginal;
}, 500);
// Stop after 5 seconds
setTimeout(() => clearInterval(flashInterval), 5000);
}Perfect for: Pomodoro, exam timers, auctions
// Countdown timer in favicon
let secondsLeft = 300; // 5 minutes
const timerInterval = setInterval(() => {
const minutes = Math.floor(secondsLeft / 60);
const seconds = secondsLeft % 60;
updateFaviconText(`${'$'}{minutes}:${'$'}{seconds.toString().padStart(2, '0')}`);
secondsLeft--;
if (secondsLeft < 0) {
clearInterval(timerInterval);
flashFavicon(); // Alert when done
}
}, 1000);Animated Favicon Best Practices
? Do This
- Use for meaningful notifications only
- Reset favicon when tab gains focus
- Keep animations subtle and professional
- Test performance impact
- Provide option to disable animations
- Use requestAnimationFrame for smooth animation
- Clear intervals/timeouts properly
- Fallback to static icon if Canvas not supported
? Avoid This
- Constant animation (annoying, battery drain)
- Too fast animation (seizure risk)
- Complex animations (performance)
- Using GIF (doesn't animate)
- Forgetting to stop animation loops
- Animating on mobile (battery concern)
- Excessive canvas operations
- Ignoring accessibility concerns
Performance Considerations
Optimize Animation Performance
Best Practices for Performance:
- Use requestAnimationFrame: Better than setInterval
- Throttle updates: Max 10-15 FPS sufficient
- Stop when tab inactive: Use Page Visibility API
- Cache canvas: Don't recreate each frame
- Limit canvas size: 32x32 or 16x16 max
- Debounce updates: Don't update too frequently
- Clean up properly: Clear intervals on unmount
- Test battery impact: Especially on mobile
Optimized Animation Example:
let animationId;
let lastUpdate = 0;
const FPS = 10; // Limit to 10 FPS
const frameDelay = 1000 / FPS;
function animate(timestamp) {
if (timestamp - lastUpdate >= frameDelay) {
updateFaviconFrame();
lastUpdate = timestamp;
}
animationId = requestAnimationFrame(animate);
}
// Start animation
animationId = requestAnimationFrame(animate);
// Stop when tab hidden
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
cancelAnimationFrame(animationId);
} else {
animationId = requestAnimationFrame(animate);
}
});
// Cleanup
function stopAnimation() {
if (animationId) {
cancelAnimationFrame(animationId);
}
}Accessibility & User Preferences
Respect User Preferences
Detect Motion Preferences:
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
// Safe to animate
startFaviconAnimation();
} else {
// Use static badge instead
updateFavicon(count);
}Important: Always respect
prefers-reduced-motion. Animated favicons can cause discomfort or trigger seizures in sensitive users.
Start with Static, Perfect Favicons
Generate professional static favicons first, then add animation with JavaScript
Generate Base Favicons