92

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.

Dark mode Wikimedia Foundation tab screenshot

Dark mode Codecademy tab screenshot

Is there a way to detect if the user is using dark mode and change the favicon?

Michael
  • 8,362
  • 6
  • 61
  • 88
agustin
  • 2,187
  • 2
  • 22
  • 40

6 Answers6

61

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.

  1. 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">
    
  2. Create a CSS media matcher:

    matcher = window.matchMedia('(prefers-color-scheme: dark)');
    matcher.addListener(onUpdate);
    onUpdate();
    
  3. 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();
      }
    }
    
Josh Lee
  • 171,072
  • 38
  • 269
  • 275
  • Also see the [Firefox progress](https://bugzilla.mozilla.org/show_bug.cgi?id=1494034) on `prefers-color-scheme`. – Michael Apr 24 '19 at 17:52
  • 2
    Update for Chrome: [dynamically updating favicon](https://web.dev/prefers-color-scheme/#reacting-on-dark-mode-changes) – Yaelet Aug 06 '19 at 07:33
  • So that example suggests setting the href property of favicons. – Josh Lee Aug 06 '19 at 10:40
  • 5
    I wish there was a better way (e.g. the browser probes the tab color and supports a attribute on the link element) because this does not only break down when using a dark theme without system wide dark-mode (like me) but it also fails in incognito tabs which are by default dark in Chrome. – 2called-chaos May 25 '20 at 14:42
  • we don't need to add a listener for this media matcher, it's better to just take the value on page load! so this line `matcher.addListener(onUpdate);` can be deleted with no problem! – Idriss AIT HAFID Dec 20 '21 at 16:17
  • But what would you do if you want your dark-theme-icon to show up in Google's SERPs accordding to the user's prefs? Does Google only index a single version? Probably? – Pieterjan Dec 21 '21 at 14:42
49

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

Red
  • 2,840
  • 1
  • 17
  • 11
  • 3
    This is a great answer and I think this will quickly become the standard way favicons are done. However this still isn't perfect yet. The problem is that favicons can be displayed over dark backgrounds in other situations other than dark mode. For example if an app has a dark theme even in light mode. Hopefully there will be a media query for if the background **is** dark rather than just the user **preferring** dark backgrounds. – Ryan Apr 26 '20 at 01:07
  • I agree there are inconsistencies, and that's why I added these issues months ago, on both Firefox and Chrome: - https://bugzilla.mozilla.org/show_bug.cgi?id=1614963 - https://bugs.chromium.org/p/chromium/issues/detail?id=1052408 – Red Apr 28 '20 at 08:21
  • This works in FF and Chrome. For Safari you should add a "mask-icon" as described here https://medium.com/swlh/are-you-using-svg-favicons-yet-a-guide-for-modern-browsers-836a6aace3df – riccardolardi Aug 13 '20 at 08:58
34

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!

Michael
  • 8,362
  • 6
  • 61
  • 88
Hashbrown
  • 12,091
  • 8
  • 72
  • 95
  • 3
    Since `addListener()` is deprecated, use `match.addEventListener('change', swap)` – cheack Jun 08 '20 at 04:37
  • 4
    I'd upvote your answer if you removed the jquery – gman Feb 15 '22 at 18:17
  • 1
    @gman anyone competent should be able to translate it, especially since they'd be using es6 with a transpiler for es5. Everyone else is for sure using jquery, so this is the simplest way to get the answer to the most people. I don't agree that SO should be a copy/paste source and if you cant use jquery and have to godforbid *read* the code you're putting in your project...**good** – Hashbrown Feb 15 '22 at 23:28
  • 3
    I haven't used jquery in > 10 yrs. I know zero devs that use it. It's out of date. I guess I downvote then for having out of date info that will mislead any new devs with outdated examples. – gman Feb 16 '22 at 06:17
  • 1
    I doubt anyone could care less about how many devs you know, you do you, fam. If you genuinely think jquery's outdated for a lot of people that use this site and it doesnt at least read basically like psuedo code for everyone else who doesnt use it (myself included, as a nodejs and react typescript dev) then I really wont even respect your opinion, tbh. Spend your -1 however, we both got heaps to spare, as long as I think Im being helpful, and at least 20 others seem to think so, then that's what Im using SO for, not picking fights and forcing worldviews. – Hashbrown Feb 16 '22 at 10:55
  • 2
    What a narrow-minded comment @gman , you can just add a javascript version of the answer if it suits you better. – Cyrille Armanger Aug 21 '22 at 16:19
  • Which browsers implemented `media` natively so far? Where can I see the progress? – Paul Oct 22 '22 at 17:13
3

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">
  • Please provide an explanation for the code you wrote, so that future readers can understand how the code is working. – Not A Bot Oct 23 '20 at 07:51
0

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';
    }
Ufukcan Eski
  • 76
  • 1
  • 3
0

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"
tem
  • 101
  • 4