A favicon that follows dark mode, from a PNG logo

Christian Drexler

The logo of this site is a single black letter on a transparent background. As a favicon in a light browser it looked right. Then I opened the site on a machine set to dark mode and the tab had no icon at all. It was there. It was black on nearly black.

The usual fixes are a white box behind the logo, which looks like a sticker, or a grey that is mediocre on both backgrounds. There is a better one. An SVG file can contain CSS, CSS can ask for the colour scheme, and browsers apply that to favicons.

From PNG to a clean path

I only had the logo as a PNG. For an icon that must stay sharp at 16 pixels and switch colour, it needs to be a vector with a single path. potrace does that conversion well for one-colour shapes. It wants a bitmap as input, so ImageMagick goes first.

magick logo.png -background white -alpha remove -threshold 50% logo.pbm
potrace logo.pbm --svg --flat -o logo.svg

The first line flattens transparency onto white and reduces the image to pure black and white. The second traces the black areas. --flat puts everything into one path, which is what makes the next step simple.

Open the result once and look at it enlarged. Tracing a small PNG produces lumpy curves. Start from the largest version of the logo you can find.

The stylesheet inside the icon

The traced file gets two small edits: a square viewBox with a little margin around the letter, and a <style> block.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
  <style>
    path { fill: #111; }
    @media (prefers-color-scheme: dark) {
      path { fill: #fff; }
    }
  </style>
  <path d="M96 64h160c106 0 192 86 192 192s-86 192-192 192H96z ..." />
</svg>

The path has no fill attribute of its own. The colour comes only from the stylesheet. In a light scheme the letter is nearly black, in a dark scheme it is white.

One thing to be clear about: the media query follows the colour scheme of the operating system or browser setting, not the actual colour of the tab strip. Somebody with a light system and a dark browser theme still gets the dark letter. That covers most people and misses a few, and there is no way to ask a browser what colour its tabs are.

The five files

In a Next.js App Router project, icons are files with fixed names in the app directory. Next generates the link tags from them. This is the set I ended up with.

app/icon.svg is the adaptive icon from above. Chrome, Edge and Firefox use it.

app/favicon.ico is the fallback for everything that does not take an SVG, with three sizes in one file.

magick logo.png -background none -define icon:auto-resize=48,32,16 favicon.ico

Safari is the main customer here. At the time of writing it does not use an SVG icon for tabs, so Safari users see the .ico. That one cannot adapt, so it should work on both backgrounds as well as a single image can. For a black logo that means choosing between a light tile behind it and weak contrast in dark mode.

app/apple-icon.png, 180 by 180 pixels, for the iPhone home screen. iOS replaces transparency with black. A black letter with a transparent background becomes a black square. This file needs a solid background.

magick -size 180x180 xc:white \( logo.png -resize 120x120 \) -gravity center -composite apple-icon.png

public/icon-192.png and public/icon-512.png for Android and for the install dialog, referenced from the manifest.

app/manifest.webmanifest, short:

{
  "name": "Example",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "browser"
}

After a build, the page head contains the links for the .ico, the SVG with type="image/svg+xml", the Apple icon and the manifest. Nothing has to be written by hand, and nothing may be: a hand-written <link rel="icon"> in the layout next to the file convention produces two competing entries.

Testing it

Favicons are cached harder than anything else in a browser, which makes testing them annoying. What worked for me:

  • Open the SVG file itself in a tab, at its own address, and switch the system between light and dark. The letter should flip while you watch. If it does not, the stylesheet is wrong, and there is no point looking at the favicon yet.
  • For the tab icon, use a private window. It starts with an empty icon cache.
  • In Chrome's developer tools, the Rendering panel has an "Emulate CSS media feature prefers-color-scheme" switch. It flips the page and the favicon without touching system settings.

To see what the server really delivers:

curl -s https://example.com/ | grep -o '<link rel="[^"]*icon[^>]*>'
curl -sI https://example.com/icon.svg | grep -i content-type

The second line must say image/svg+xml. An SVG served as plain text is ignored as an icon.

Worth doing

It is a small thing. But a favicon is the one piece of a site that stays visible while the reader is on another tab, and a missing one looks like a site somebody forgot to finish. Plenty of people browse in dark mode, developers more than most. For a logo that is a single dark shape, the adaptive SVG is the only version that looks intended in both.

If your logo has its own background or several colours, you do not need any of this. If it is one dark letter or one dark mark on transparency, open your site in a dark browser once. You might be looking at an empty tab too.