Google Chrome 73 has been released, and it adds "dark mode" support to the browser. I notice that a lot of favicons look bad now.
Is there a way to detect if the user is using dark mode and change the favicon?
Google Chrome 73 has been released, and it adds "dark mode" support to the browser. I notice that a lot of favicons look bad now.
Is there a way to detect if the user is using dark mode and change the favicon?
Adding and removing an icon from the document’s head works in Firefox but not Safari:
Chrome is still implementing (prefers-color-scheme: dark), so the jury’s still out. https://crbug.com/889087. In Chrome 76 with --enable-blink-features=MediaQueryPrefersColorScheme, this correctly sets the icon when the page is loaded, but does not respond dynamically to changes in dark mode.
Safari adds a grey background to dark icons in dark mode (for example, Wikimedia Foundation, Github), so this workaround isn't necessary for legibility.
Add two link rel=icon elements with ids for later:
<link rel="icon" href="a.png" id="light-scheme-icon">
<link rel="icon" href="b.png" id="dark-scheme-icon">
Create a CSS media matcher:
matcher = window.matchMedia('(prefers-color-scheme: dark)');
matcher.addListener(onUpdate);
onUpdate();
Add/remove the elements from the document's head:
lightSchemeIcon = document.querySelector('link#light-scheme-icon');
darkSchemeIcon = document.querySelector('link#dark-scheme-icon');
function onUpdate() {
  if (matcher.matches) {
    lightSchemeIcon.remove();
    document.head.append(darkSchemeIcon);
  } else {
    document.head.append(lightSchemeIcon);
    darkSchemeIcon.remove();
  }
}
 
    
    CSS has a theme mode detection using prefers-color-scheme media feature:
@media (prefers-color-scheme: dark) {
  ...
}
With that in mind, nowadays you can use an SVG as a favicon for your website:
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
Then you can update the SVG favicon design using the CSS prefers-color-scheme media feature. Below is an SVG rectangle with rounded corners, which has a different color, depending on the active theme:
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg">
  <style>
    rect {
      fill: green;
    }
    @media (prefers-color-scheme: dark) {
      rect {
        fill: red;
      }
    }
  </style>
  <rect width="50" height="50" rx="5"/>
</svg>
Now, considering the current browser support for the SVG favicon, a fallback is required for the older browsers:
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.png" type="image/png">
<!-- favicon.ico in the root -->
From https://catalin.red/svg-favicon-light-dark-theme/
Here's a demo too: https://codepen.io/catalinred/pen/vYOERwL
 
    
    To make it a little more generic than Josh's answer, try this whilst the browsers still get around to implementing media natively. (Notice no hardcoded number of themes, ids, or media-queries in the JS; it's all kept in the HTML.)
<link rel="icon" href="/favicon.ico?light" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="/favicon.ico?dark"  media="(prefers-color-scheme:dark)">
<link rel="icon" href="/favicon.ico?light" media="(prefers-color-scheme:light)">
$(document).ready(function() {
    if (!window.matchMedia)
        return;
    var current = $('head > link[rel="icon"][media]');
    $.each(current, function(i, icon) {
        var match = window.matchMedia(icon.media);
        function swap() {
            if (match.matches) {
                current.remove();
                current = $(icon).appendTo('head');
            }
        }
        match.addListener(swap);
        swap();
    });
});
The upshot is that once that attribute is supported, you just need to remove the Javascript and it'll still work.
I deliberately split /favicon.ico?light into two tags instead of a single one with media="(prefers-color-scheme: no-preference), (prefers-color-scheme:light)" because some browsers that don't support media permanently pick the first rel="icon" they see… and others pick the last!
The easiest option is to change the source when you change the mode on your computer.
var element = document.querySelector("link[rel='icon']");
const darkModeListener = (event) => {
if (event.matches) {
  console.log("dark");
  element.setAttribute("href","img/favicon-dark.svg");
} else {
  console.log("light");
  element.setAttribute("href","img/favicon-light.svg");
}
};
// Update favicon on Mode change 
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeListener);
// Check Mode on load
if (window.matchMedia && window.matchMedia('(prefers-color-scheme:      dark)').matches) {
  element.setAttribute("href","img/favicon-dark.svg");
} else {
  element.setAttribute("href","img/favicon-light.svg");
}
 But if you have a multi-device favicon then you need to do something like this ...
// Switch Favicon on Dark/Light Mode
        let colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)');
            const setColorScheme = e => {
              if (e.matches) {
                // Dark
                var favicon = document.querySelectorAll('link[data-type="favicon"]');
                var i = favicon.length;
                while (i--) {              
                  favicon[i].setAttribute('href', favicon[i].dataset.dark);
                }
              } else {
                // Light
                var favicon = document.querySelectorAll('link[data-type="favicon"]');
                var i = favicon.length;
                while (i--) {              
                  favicon[i].setAttribute("href", favicon[i].dataset.light);
                }
              }
            }
           
           setColorScheme(colorSchemeQueryList);
           colorSchemeQueryList.addListener(setColorScheme);<link rel="apple-touch-icon" sizes="180x180" href="Images/favicon/light/apple-touch-icon.png" data-type="favicon" data-light="Images/favicon/light/apple-touch-icon.png" data-dark="Images/favicon/dark/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="Images/favicon/light/favicon-32x32.png" data-type="favicon" data-light="Images/favicon/light/favicon-32x32.png" data-dark="Images/favicon/dark/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="Images/favicon/light/favicon-16x16.png" data-type="favicon" data-light="Images/favicon/light/favicon-16x16.png" data-dark="Images/favicon/dark/favicon-16x16.png">
  <link rel="favicon" href="Images/favicon/light/site.webfavicon" data-type="favicon" data-light="Images/favicon/light/site.webfavicon" data-dark="Images/favicon/dark/site.webfavicon">
  <link rel="mask-icon" href="Images/favicon/light/safari-pinned-tab.svg" color="#000000" data-type="favicon" data-light="Images/favicon/light/safari-pinned-tab.svg" data-dark="Images/favicon/dark/safari-pinned-tab.svg">This is how I solved it, hope it helps you.
const link = document.createElement('link');
    link.rel = 'shortcut icon';
    document.head.appendChild(link);
    const browserColorScheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
    if (browserColorScheme == "dark") {
        link.href = 'assets/upload/favicon_light.svg';
    } else {
        link.href = 'assets/upload/favicon.svg';
    }
 
    
    Here's a simplified version of Gaius Galerius Valerius Maximi's answer when there's only one favicon. Here I used the modern ES6 ternary operator.
const icon = document.querySelector("link[rel='icon']")
// Update favicon on Mode change 
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => icon.href = e.matches ? "path/to/favicon-white.png" : "path/to/favicon-dark.png")
// Check Mode on load
icon.href = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? "path/to/favicon-white.png" : "path/to/favicon-dark.png"