How to Create Adaptive Favicons for Light and Dark Modes: Complete Developer Guide

Favicon.im

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

  1. Maintain brand recognition - Keep your core design elements consistent
  2. Test at multiple sizes - 16x16, 32x32, and 180x180 pixels
  3. Use simple shapes - Complex details disappear at small sizes
  4. 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:

  1. 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)">
    
  2. 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.

Check Your Favicon

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.

15M+
Monthly Favicon Requests
100%
Free Forever