How to Create Adaptive Favicons for Light and Dark Modes: Complete Developer Guide
Modern websites need to adapt to user preferences, and favicon theming is an often-overlooked detail that can significantly enhance user experience. When users switch between light and dark modes, your favicon should adapt accordingly to maintain visual consistency.
This comprehensive guide covers everything from simple HTML-only solutions to advanced JavaScript implementations across popular frameworks. Whether you're building a static site or a complex web application, you'll find the right approach for your project.
Method 1: HTML-Only Solution (Recommended for Most Sites)
The HTML-only approach is the most reliable method and requires no JavaScript. It uses CSS media queries within the media attribute of favicon link tags to automatically switch favicons based on the user's system preference.
Why this method works best:
- ✅ Zero JavaScript required
- ✅ Works immediately on page load
- ✅ Supported by all modern browsers
- ✅ No performance overhead
Basic Implementation
<head>
<!-- Default favicon (fallback for unsupported browsers) -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon">
<!-- Light mode favicon -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon" media="(prefers-color-scheme: light)">
<!-- Dark mode favicon -->
<link rel="icon" href="/favicon-dark.ico" type="image/x-icon" media="(prefers-color-scheme: dark)">
</head>
Complete Multi-Size Implementation
For comprehensive device support, implement multiple sizes with theme variants:
<head>
<!-- Default favicons (fallback) -->
<link rel="icon" type="image/x-icon" href="/favicon-light.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png">
<!-- Light mode favicons -->
<link rel="icon" type="image/x-icon" href="/favicon-light.ico" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-light-16x16.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png" media="(prefers-color-scheme: light)">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-light.png" media="(prefers-color-scheme: light)">
<!-- Dark mode favicons -->
<link rel="icon" type="image/x-icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-dark-16x16.png" media="(prefers-color-scheme: dark)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-dark-32x32.png" media="(prefers-color-scheme: dark)">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-dark.png" media="(prefers-color-scheme: dark)">
<!-- SVG favicons with embedded CSS -->
<link rel="icon" type="image/svg+xml" href="/favicon-adaptive.svg">
</head>
Adaptive SVG Favicon
Create a single SVG favicon that adapts to the color scheme automatically:
<!-- favicon-adaptive.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<style>
.light-mode { fill: #000000; }
.dark-mode { fill: #ffffff; }
@media (prefers-color-scheme: dark) {
.light-mode { display: none; }
}
@media (prefers-color-scheme: light) {
.dark-mode { display: none; }
}
</style>
<!-- Light mode design -->
<circle class="light-mode" cx="16" cy="16" r="12"/>
<text class="light-mode" x="16" y="20" text-anchor="middle" fill="#fff" font-size="14">L</text>
<!-- Dark mode design -->
<circle class="dark-mode" cx="16" cy="16" r="12"/>
<text class="dark-mode" x="16" y="20" text-anchor="middle" fill="#000" font-size="14">D</text>
</svg>
Method 2: JavaScript Implementation
When you need dynamic favicon switching beyond system preferences—such as custom theme toggles or real-time updates—JavaScript provides the flexibility you need.
Use JavaScript when:
- 🎯 You have custom theme controls
- 🎯 You need to sync with your app's theme state
- 🎯 You want to update favicons without page refresh
- 🎯 You're building a single-page application
Basic JavaScript Approach
// Function to update favicon based on theme
function updateFavicon(theme) {
const favicon = document.querySelector('link[rel="icon"]') ||
document.createElement('link');
favicon.rel = 'icon';
favicon.type = 'image/png';
favicon.href = theme === 'dark' ? '/favicon-dark.png' : '/favicon-light.png';
if (!document.querySelector('link[rel="icon"]')) {
document.head.appendChild(favicon);
}
}
// Listen for system theme changes
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// Set initial favicon
updateFavicon(mediaQuery.matches ? 'dark' : 'light');
// Listen for changes
mediaQuery.addEventListener('change', (e) => {
updateFavicon(e.matches ? 'dark' : 'light');
});
}
Advanced JavaScript with Multiple Sizes
class FaviconManager {
constructor() {
this.sizes = [
{ size: '16x16', selector: 'link[rel="icon"][sizes="16x16"]' },
{ size: '32x32', selector: 'link[rel="icon"][sizes="32x32"]' },
{ size: '180x180', selector: 'link[rel="apple-touch-icon"]' }
];
this.init();
}
init() {
// Set initial theme
this.updateTheme(this.getSystemTheme());
// Listen for system changes
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
this.updateTheme(e.matches ? 'dark' : 'light');
});
}
}
getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
}
updateTheme(theme) {
this.sizes.forEach(({ size, selector }) => {
let link = document.querySelector(selector);
if (!link) {
link = document.createElement('link');
link.rel = size === '180x180' ? 'apple-touch-icon' : 'icon';
link.type = 'image/png';
if (size !== '180x180') link.sizes = size;
document.head.appendChild(link);
}
link.href = `/favicon-${theme}-${size}.png`;
});
// Update default ico file
let icoLink = document.querySelector('link[rel="icon"][type="image/x-icon"]');
if (!icoLink) {
icoLink = document.createElement('link');
icoLink.rel = 'icon';
icoLink.type = 'image/x-icon';
document.head.appendChild(icoLink);
}
icoLink.href = `/favicon-${theme}.ico`;
}
// Method to manually set theme (for custom theme toggles)
setTheme(theme) {
this.updateTheme(theme);
}
}
// Initialize
const faviconManager = new FaviconManager();
// Export for manual theme switching
window.faviconManager = faviconManager;
Method 3: Framework Integration
Modern frameworks offer elegant ways to handle favicon theming. Here's how to implement adaptive favicons in the most popular JavaScript frameworks.
React Implementation
import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
function AdaptiveFavicon() {
const [theme, setTheme] = useState('light');
useEffect(() => {
// Check system preference
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setTheme(mediaQuery.matches ? 'dark' : 'light');
// Listen for changes
const handleChange = (e) => {
setTheme(e.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return (
<Helmet>
<link rel="icon" type="image/x-icon" href={`/favicon-${theme}.ico`} />
<link rel="icon" type="image/png" sizes="32x32" href={`/favicon-${theme}-32x32.png`} />
<link rel="apple-touch-icon" sizes="180x180" href={`/apple-touch-icon-${theme}.png`} />
</Helmet>
);
}
Vue 3 Implementation
<template>
<div>
<!-- Your app content -->
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { useHead } from '@unhead/vue'
const isDark = ref(false)
const updateFavicon = () => {
const theme = isDark.value ? 'dark' : 'light'
useHead({
link: [
{ rel: 'icon', type: 'image/x-icon', href: `/favicon-${theme}.ico` },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: `/favicon-${theme}-32x32.png` },
{ rel: 'apple-touch-icon', sizes: '180x180', href: `/apple-touch-icon-${theme}.png` }
]
})
}
onMounted(() => {
// Check system preference
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
isDark.value = mediaQuery.matches
// Listen for changes
mediaQuery.addEventListener('change', (e) => {
isDark.value = e.matches
})
}
updateFavicon()
})
watch(isDark, updateFavicon)
</script>
Nuxt 3 Implementation
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
innerHTML: `
(function() {
const updateFavicon = (isDark) => {
const theme = isDark ? 'dark' : 'light';
const links = [
{ rel: 'icon', type: 'image/x-icon', href: \`/favicon-\${theme}.ico\` },
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: \`/favicon-\${theme}-32x32.png\` }
];
links.forEach(linkData => {
let link = document.querySelector(\`link[rel="\${linkData.rel}"][sizes="\${linkData.sizes || 'any'}"]\`);
if (!link) {
link = document.createElement('link');
Object.assign(link, linkData);
document.head.appendChild(link);
} else {
link.href = linkData.href;
}
});
};
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
updateFavicon(mediaQuery.matches);
mediaQuery.addEventListener('change', e => updateFavicon(e.matches));
}
})();
`
}
]
}
}
})
Method 4: CSS-in-JS Favicon (Advanced)
Generate favicons dynamically using Canvas and CSS colors:
class DynamicFaviconGenerator {
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 32;
this.canvas.height = 32;
}
generateFavicon(theme) {
const colors = {
light: { bg: '#ffffff', text: '#000000' },
dark: { bg: '#000000', text: '#ffffff' }
};
const { bg, text } = colors[theme];
// Clear canvas
this.ctx.clearRect(0, 0, 32, 32);
// Draw background
this.ctx.fillStyle = bg;
this.ctx.fillRect(0, 0, 32, 32);
// Draw border
this.ctx.strokeStyle = text;
this.ctx.lineWidth = 2;
this.ctx.strokeRect(2, 2, 28, 28);
// Draw icon (example: letter or symbol)
this.ctx.fillStyle = text;
this.ctx.font = 'bold 20px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText('🌙', 16, 16);
return this.canvas.toDataURL('image/png');
}
updateFavicon(theme) {
const dataUrl = this.generateFavicon(theme);
let link = document.querySelector('link[rel="icon"]');
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/png';
document.head.appendChild(link);
}
link.href = dataUrl;
}
}
// Usage
const generator = new DynamicFaviconGenerator();
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
generator.updateFavicon(mediaQuery.matches ? 'dark' : 'light');
mediaQuery.addEventListener('change', e => {
generator.updateFavicon(e.matches ? 'dark' : 'light');
});
Design Best Practices
Creating effective adaptive favicons requires careful attention to design principles and user experience.
Color Contrast and Visibility
Light Mode Favicon Design:
- ✅ Use dark elements (text, icons) on transparent or light backgrounds
- ✅ Aim for WCAG AA contrast ratios (4.5:1 minimum)
- ✅ Test appearance on white browser tabs and bookmark bars
- ✅ Ensure clarity at 16x16 pixels (smallest common size)
Dark Mode Favicon Design:
- ✅ Use light elements on transparent or dark backgrounds
- ✅ Test visibility against dark browser themes
- ✅ Avoid pure white (#ffffff) - use off-white (#f0f0f0) for better balance
- ✅ Consider subtle shadows or outlines for definition
Design Consistency Tips
- Maintain brand recognition - Keep your core design elements consistent
- Test at multiple sizes - 16x16, 32x32, and 180x180 pixels
- Use simple shapes - Complex details disappear at small sizes
- Consider colorblind users - Don't rely solely on color for differentiation
File Naming Convention
Organize your favicon files with clear naming:
/public/
├── favicon-light.ico
├── favicon-dark.ico
├── favicon-light-16x16.png
├── favicon-dark-16x16.png
├── favicon-light-32x32.png
├── favicon-dark-32x32.png
├── apple-touch-icon-light.png
├── apple-touch-icon-dark.png
└── favicon-adaptive.svg
Browser Compatibility
Modern Browser Support for Adaptive Favicons
| Browser | Media Query Support | Notes |
|---|---|---|
| Chrome 76+ | ✅ Full support | Works perfectly |
| Firefox 67+ | ✅ Full support | Excellent implementation |
| Safari 12.1+ | ✅ Full support | iOS Safari included |
| Edge 79+ | ✅ Full support | Chromium-based Edge |
| Internet Explorer | ❌ No support | Use JavaScript fallback |
Market Coverage: These versions cover ~95% of global browser usage as of 2025.
Fallback Strategy
<!-- Always provide fallbacks -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon">
<!-- Enhanced support for modern browsers -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon" media="(prefers-color-scheme: light)">
<link rel="icon" href="/favicon-dark.ico" type="image/x-icon" media="(prefers-color-scheme: dark)">
<!-- JavaScript fallback for older browsers -->
<script>
if (!window.matchMedia || !CSS.supports('(prefers-color-scheme: dark)')) {
// Load favicon based on time of day or other heuristics
const hour = new Date().getHours();
const isDark = hour < 6 || hour > 18;
document.querySelector('link[rel="icon"]').href =
isDark ? '/favicon-dark.ico' : '/favicon-light.ico';
}
</script>
Testing and Validation
Manual Testing Checklist
- [ ] Test in light mode (system preference)
- [ ] Test in dark mode (system preference)
- [ ] Verify favicon changes immediately on system theme switch
- [ ] Check different browsers (Chrome, Firefox, Safari, Edge)
- [ ] Test on mobile devices
- [ ] Validate fallback behavior in older browsers
Automated Testing
// Test script for favicon theme switching
function testFaviconThemes() {
const tests = [
{ theme: 'light', expected: '/favicon-light.ico' },
{ theme: 'dark', expected: '/favicon-dark.ico' }
];
tests.forEach(({ theme, expected }) => {
// Mock media query
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: query.includes('dark') ? theme === 'dark' : theme === 'light',
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
})),
});
// Trigger update
updateFavicon(theme);
// Assert
const favicon = document.querySelector('link[rel="icon"]');
expect(favicon.href).toContain(expected);
});
}
Performance Optimization
Preload Theme Favicons
<!-- Preload both theme favicons for instant switching -->
<link rel="preload" as="image" href="/favicon-light.ico">
<link rel="preload" as="image" href="/favicon-dark.ico">
Minimize File Sizes
- Keep ICO files under 1KB
- Optimize PNG files with tools like TinyPNG
- Use SVG for simple geometric designs
- Consider WebP format for modern browsers
Caching Strategy
# Nginx configuration for favicon caching
location ~* \.(ico|png|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
}
Troubleshooting Common Issues
Favicon Not Switching Between Themes
Symptoms: Favicon remains the same regardless of system theme changes
Common Causes & Solutions:
-
Browser Cache Issues
<!-- Add cache-busting parameters --> <link rel="icon" href="/favicon-light.ico?v=2025" media="(prefers-color-scheme: light)"> <link rel="icon" href="/favicon-dark.ico?v=2025" media="(prefers-color-scheme: dark)"> -
Incorrect Media Query Syntax
<!-- ❌ Wrong --> <link rel="icon" href="/favicon-dark.ico" media="dark"> <!-- ✅ Correct --> <link rel="icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
Multiple Favicons Loading Simultaneously
Symptoms: Network tab shows multiple favicon requests
Solution: Use JavaScript to replace instead of adding:
function replaceFavicon(href) {
// Remove all existing favicon links
document.querySelectorAll('link[rel*="icon"]').forEach(link => link.remove());
// Add new favicon
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/x-icon';
link.href = href;
document.head.appendChild(link);
}
SVG Favicons Not Displaying
Symptoms: SVG favicon works in some browsers but not others
Root Cause: Limited SVG favicon support in older browsers
Solution: Always provide PNG fallbacks:
<!-- Modern browsers: SVG with media queries -->
<link rel="icon" type="image/svg+xml" href="/favicon-adaptive.svg">
<!-- Fallback: PNG for older browsers -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-dark-32x32.png" media="(prefers-color-scheme: dark)">
Advanced Techniques
Theme-Aware Notification Badges
class NotificationFavicon {
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 32;
this.canvas.height = 32;
this.baseIcons = {
light: '/favicon-light-32x32.png',
dark: '/favicon-dark-32x32.png'
};
}
async drawWithBadge(theme, count) {
const baseIcon = new Image();
baseIcon.src = this.baseIcons[theme];
return new Promise(resolve => {
baseIcon.onload = () => {
this.ctx.clearRect(0, 0, 32, 32);
this.ctx.drawImage(baseIcon, 0, 0, 32, 32);
if (count > 0) {
// Draw notification badge
const badgeSize = 12;
const x = 32 - badgeSize;
const y = 0;
// Badge background
this.ctx.fillStyle = '#ff4444';
this.ctx.beginPath();
this.ctx.arc(x + badgeSize/2, y + badgeSize/2, badgeSize/2, 0, 2 * Math.PI);
this.ctx.fill();
// Badge text
this.ctx.fillStyle = 'white';
this.ctx.font = '8px Arial';
this.ctx.textAlign = 'center';
this.ctx.textBaseline = 'middle';
this.ctx.fillText(
count > 9 ? '9+' : count.toString(),
x + badgeSize/2,
y + badgeSize/2
);
}
resolve(this.canvas.toDataURL());
};
});
}
async updateWithNotification(count = 0) {
const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
const dataUrl = await this.drawWithBadge(theme, 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 notificationFavicon = new NotificationFavicon();
notificationFavicon.updateWithNotification(3); // Show badge with count 3
Summary and Next Steps
Adaptive favicons represent a small but impactful way to enhance user experience. They show attention to detail and respect for user preferences, contributing to a more polished and professional website.
Choose the Right Method for Your Project
| Method | Best For | Complexity | Performance |
|---|---|---|---|
| HTML-only | Static sites, blogs, marketing pages | Low | Excellent |
| JavaScript | SPAs, custom themes, dynamic updates | Medium | Good |
| Framework integration | React/Vue/Nuxt applications | Medium | Good |
| Advanced techniques | Notification systems, real-time updates | High | Variable |
Implementation Checklist
Before deploying your adaptive favicon system:
- [ ] Create both light and dark favicon versions
- [ ] Test in multiple browsers (Chrome, Firefox, Safari, Edge)
- [ ] Verify switching works with system theme changes
- [ ] Test on mobile devices (iOS Safari, Android Chrome)
- [ ] Optimize file sizes (keep under 1KB for ICO files)
- [ ] Add appropriate fallbacks for older browsers
- [ ] Validate implementation with tools like Favicon.im
Performance Impact
When implemented correctly, adaptive favicons have minimal performance impact:
- HTML-only method: Zero JavaScript overhead
- File size impact: ~2-4KB total (light + dark versions)
- Loading time: Negligible when properly cached
Going Further
Consider these advanced optimizations:
- Preload critical favicon assets for instant switching
- Use WebP format for modern browsers (with PNG fallbacks)
- Implement dynamic favicon badges for notifications
- Add favicon animations for special events or statuses
By implementing adaptive favicons thoughtfully, you create a more cohesive and user-friendly web experience that adapts to modern user preferences.
Use favicon.im to quickly check if your favicon is configured correctly. Our free tool ensures your website's favicon displays properly across all browsers and devices.
Free Public Service
Favicon.im is a completely free public service trusted by developers worldwide.