Updating your website favicon dynamically with dark mode

UPDATE: Whilst the method below still works absolutely fine, SVG favicon support is much more prevalent in browsers nowadays. The same functionality as below can be accomplished with a single SVG favicon and a prefers-color-scheme media query within it. See this post by Thomas Steiner for more info.

If you do still need to use separate ico files, you can accomplish the same thing as below, but without any JavaScript, by using the following snippet:

<link rel="icon" href="/favicon.ico" media="(prefers-color-scheme:no-preference)">
<link rel="icon" href="/favicon-dark.ico" media="(prefers-color-scheme:dark)">
<link rel="icon" href="/favicon.ico" media="(prefers-color-scheme:light)">

For an older method using JavaScript, continue reading below.

Dark mode is everywhere now (and I love it). It's in Windows 10, Mac OS Mojave, iOS 13, and Android 10, the vast majority of which support the prefers-color-scheme media query.

Many websites have already implemented dark themes by using this media query, but with browsers like Chrome, website favicons often get forgotten about, resulting in illegible favicons on some sites with dark mode.

For example, take a look at how my website's favicon looked when in dark mode in Chrome 78, on Windows 10. You can see the J from my favicon is pretty much illegible due to the dark tab background, compared to a lighter tab background in light mode.

Comparing the Chrome dark vs light mode UI with static favicon

Dynamically switching favicon

My blog injects the following favicons into my page. These are the two favicons we want to update to dark mode, when enabled.

<link rel="icon" type="image/png" href="/assets/images/favicon-192.png" sizes="192x192">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">

For this example, I've added a favicon-dark.ico and /assets/images/favicon-dark-192.png to my blog for use when in dark mode.

To update these dynamically, we'll need to use a little JavaScript, and the window.matchMedia API. This works pretty much identically to a media query within CSS.

var darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function handleDarkmode(e){
	var darkModeOn = e.matches; // true if dark mode is enabled
	var favicon = document.querySelector('link[rel="shortcut icon"]'); // get favicon-192.png element
	var largeFavicon = document.querySelector('link[rel="icon"]'); // get favicon.ico element
	if(!favicon || !largeFavicon){
		return; // where are our favicon elements???
	// replace icons with dark/light themes as appropriate
		favicon.href = '/favicon-dark.ico';
		largeFavicon.href = '/assets/images/favicon-dark-192.png';
		favicon.href = '/favicon.ico';
		largeFavicon.href = '/assets/images/favicon-192.png';

As you can see from the above code, I first check if dark mode is enabled via window.matchMedia('(prefers-color-scheme: dark)'). I then define a function, handleDarkmode which will automatically update our favicons to dark variants, when dark mode is enabled.

I run this function immediately on page load with the original result of the dark mode query, and then setup a listener via darkModeMediaQuery.addListener(handleDarkmode), which will ensure the favicon is continually updated every time the result of this media query changes. Watch the favicon in the gif below to see it update dynamically as the system colour scheme changes.

Dynamic favicons in Chrome with dark/light mode UI
Dynamically updating favicon with dark/light mode

You could extend this to also update things like theme-color and your apple-touch-icon etc. but this was "good enough" for my use-case.

You've successfully subscribed to James Ross
Great! Next, complete checkout for full access to James Ross
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.