Light & Dark Mode Favicons

Favicons are one of the basic building blocks of building a website and adding one isn’t anything new, but gone are the days where we’re forced to use .ico files, where instead, we can take advantage of formats like SVG to create adaptive experiences based on your visitors system appearance.

Table of Contents

YouTube Preview
View on YouTube

What are Adaptive Images?

An adaptive image is an image that will change to to its environment.

This can mean a lot of things for standard web experiences, such as Reduced Motion or even Network and GPU speed, but it also includes the ability to change based on your visitor’s system preference of light mode or dark mode.

How do Adaptive Favicons work?

To create adaptive favicons, we have to go beyond traditional .ico files.

Modern browser support using SVG images as a favicon and with SVG comes additional capabilities such as using an embedded style block to change styles, including using media queries based on system preference.

SVG icon with embedded style tag and media query

This opens up a lot of possibilities for being able to create the experience we want for our favicons depending on the environment.

What are we going to build?

We’re going to see how we can set up SVG favicons on a website. Taking that further, we’ll see how we can modify that icon to adapt to light or dark mode environments to change colors.

This should work in any framework or even standard HTML page, as this is plain HTML and CSS!

Step 1: Adding an SVG Favicon to a website

Getting started, let’s set up our website to use an SVG favicon.

The trickiest part here is simply having access to an SVG version of your image.

If you’re working in a tool like Figma, you have the ability to easily export your image as SVG.

But once you have your SVG file, we can use a standard Link tag to reference our icon:

<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />

Note: If you have a favicon.ico file, comment it out for now.

And your SVG favicon should now be showing in the browser!

SVG favicon in browser tab

Or is it?

Depending on which browser you’re using, and your previous setup, you may or may not be seeing the favicon.

Particularly, Safari has limited support for SVG icons, so you may not being seeing the right thing, but let’s fix that.

Step 2: Setting up a fallback .ico file

In the event that our SVG icon isn’t supported, it’s important to have a backup .ico file set up.

While .ico files aren’t as straightforward to generate as an SVG file, you can head over to Real Favicon Generator and easily create one along with some other helpful files for icons.

But when it comes to linking to our .ico file, it’s important to correctly configure it.

<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />

Specifically note the sizes and type attributes defined here, as they’re important in making sure the browsers properly pick these up.

Setting sizes="48x48" for our .ico file is playing on a browser quirk, where Chrome isn’t going to look for that size, so it essentially ignores it, and instead picks up the SVG file, where Safari will use that .ico file.

Without that sizes attribute, Chrome will find the .ico file and instead use that, which is not what we want.

Shoutout to Masa Kudamatsu for a comprehensive breakdown of how this all works including the optimal favicon setup.

And with that, you should now have your favicon properly set up between the different browsers.

Favicon showing in both Chrome and Safari

Step 3: Adaptive SVG favicons for Light and Dark Mode

Now that we’re properly serving SVG favicons to browsers that support it, we can take advantage of the ability to embed CSS in a Style block and use media queries to change colors based on the color scheme.

Let’s look at this basic example of a rocket icon from Lucide Icons:

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" stroke="black" />
  <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" stroke="black" />
  <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" stroke="black" />
  <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" stroke="black" />
</svg>

Setting this up we can see it shows up as a black rocket.

Black rocket favicon

If we look at the far right of each <path we can see stroke="black" which defines the color.

Now to start off, we can define this color a little bit differently, and you guessed it, we can use CSS!

So instead of using the stroke attribute, we can use CSS to define the color.

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <style>
  path { stroke: black; }
  </style>
  <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
  <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
  <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
  <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</svg>

And reloading the browser, it should look exactly the same as before.

But here’s where thing’s get interesting.

The first <path in the SVG file is the “fire” of the rocket, so what if we colored that orange?

If we add a class (or ID) to the <path, we can target that specifically for styles:

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <style>
  path { stroke: black; }
  .booster { stroke: #f57c00; }
  </style>
  <path class="booster" d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
  <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
  <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
  <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</svg>

And now we can see our rocket has orange fire!

Rocket SVG icon with orange fire

Using this same concept, we can now use media queries to color it differently based on the theme.

Specifically we can use prefers-color-scheme and target dark mode.

Updating our file, we’ll now target the path to be white if we’re in dark mode so we can see it properly.

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <style>
  path { stroke: black; }
  @media (prefers-color-scheme: dark) {
    path { stroke: white; }
  }
  .booster { stroke: #f57c00; }
  </style>
  <path class="booster" d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
  <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
  <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
  <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
</svg>

If we refresh the page in light mode, it should still look the same, but if we toggle our system appearance to dark mode, and importantly refresh the page so the updated favicon version kicks in, we should see the dark mode version!

Dark mode favicon version

This opens up a lot of possibilities for how we can style our icons differently based on the theme, to make sure it’s able to show up exactly how we want it.

What’s next?

Updating favicon on inactive tabs

Another option for customizing your favicon is updating it when a tab is inactive.

To do this we can use some simple JavaScript that listens for the visibilitychange event:

function updateFavicon() {
    const favicon = document.querySelector('link[rel="icon"]');
    const path = document.hidden ? '/favicon-inactive.ico' : '/favicon.ico';
    favicon?.setAttribute('href', path)
}
document.addEventListener('visibilitychange', updateFavicon)

However, note we’ll need to use a separate icon file for this one.

Animating favicons

Using a similar approach to inactive tabs, you can take it a step further and even animate your favicons.

This should be done with caution as it can create a weird experience and potentially hurt performance. It’s also not supported in every browser, so its something that shouldn’t be relied on.