Building an Adaptive Favicon

How to create a single SVG favicon that automatically switches between light and dark mode — no JavaScript required.

· 8 min read

Overview

A favicon is the small icon that appears in browser tabs, bookmarks, history, and link previews. Traditionally a 16x16 .ico file, it is often treated as an afterthought — a single static image that looks fine on a white background and becomes nearly invisible when the browser switches to dark mode.

Modern browsers support SVG favicons. Because SVG is a text-based vector format it can embed CSS, including @media queries. This means you can ship a single favicon.svg that responds to prefers-color-scheme, showing a light-mode icon to users with light system themes and a dark-mode icon to everyone else — exactly like your website's own UI does.

The result is a favicon that feels polished and intentional on every device: in browser tabs, pinned tabs, reader apps, bookmark bars, and link previews.

Browser support: SVG favicons with embedded media queries are supported in Firefox and all Chromium-based browsers (Chrome, Edge, Brave, Arc, …). Safari does not yet render SVG favicons in tabs, so a fallback .ico is still essential.

How It Works

The key is a prefers-color-scheme CSS media query embedded directly inside the SVG's <style> block. The browser evaluates the media query the same way it does in a stylesheet, so the icon updates instantly whenever the user changes their system theme — no page reload, no JavaScript.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    #light { display: block; }
    #dark  { display: none;  }

    @media (prefers-color-scheme: dark) {
      #light { display: none;  }
      #dark  { display: block; }
    }
  </style>

  <!-- shown in light mode -->
  <g id="light"> … </g>

  <!-- shown in dark mode -->
  <g id="dark"> … </g>
</svg>

When the OS is in light mode the #dark group is hidden. When it switches to dark mode the media query fires, hiding #light and revealing #dark. Pure CSS — zero dependencies.

Step 1 — Create favicon.svg

Create a new file called favicon.svg at the root of your project, next to your existing favicon.ico. The outer <svg> tag is the equivalent of the <html> tag for your icon. Give it a square viewBox — favicons are always square:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">

</svg>

You need two versions of your artwork — one designed for light backgrounds and one for dark backgrounds. If your icon comes from a design tool like Figma or Illustrator, export both as SVG, run each through SVGLite to strip unnecessary markup, and paste the cleaned paths inside.

Step 2 — Add Light and Dark Styles

Add a <style> block inside the SVG. Define your light-mode appearance as the default, then override it inside a @media (prefers-color-scheme: dark) block. There are two common approaches.

Color-based approach — same shape, different colors

The simplest case: the icon shape stays the same but its fill and stroke colors change with the theme.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    .bg { fill: #ffffff; }
    .fg { fill: #0f172a; }

    @media (prefers-color-scheme: dark) {
      .bg { fill: #0f172a; }
      .fg { fill: #ffffff; }
    }
  </style>

  <rect class="bg" width="32" height="32" rx="4"/>
  <text class="fg" x="16" y="22" font-size="18"
        text-anchor="middle" font-family="sans-serif">A</text>
</svg>

Visibility-based approach — two separate icons

When your light and dark icons are structurally different, wrap each in its own <g> group and toggle visibility with CSS. This is the approach Get Adaptive Favicon uses when you upload separate light and dark images — it wraps them in a single SVG and wires up the CSS automatically.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    #light { display: block; }
    #dark  { display: none;  }

    @media (prefers-color-scheme: dark) {
      #light { display: none;  }
      #dark  { display: block; }
    }
  </style>

  <g id="light">
    <rect width="32" height="32" rx="4" fill="#f8fafc"/>
    <circle cx="16" cy="16" r="8" fill="#334155"/>
  </g>

  <g id="dark">
    <rect width="32" height="32" rx="4" fill="#1e293b"/>
    <circle cx="16" cy="16" r="8" fill="#94a3b8"/>
  </g>
</svg>

Step 3 — Link Favicons From HTML

Inside the <head> of your HTML, reference the legacy .ico fallback, the adaptive SVG, and PNG variants:

<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="32x32"
      media="(prefers-color-scheme: dark)" href="/favicon-dark-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="16x16"
      media="(prefers-color-scheme: dark)" href="/favicon-dark-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">

Order matters

Browsers evaluate <link rel="icon"> tags and pick the most capable format they support. By placing favicon.svg after favicon.ico, capable browsers (Firefox, Chromium) prefer the SVG while older browsers silently fall back to .ico.

The media="(prefers-color-scheme: dark)" attributes on the PNG links add another compatibility layer for browsers that support PNG favicons with media queries but not SVG favicons.

Verify it in DevTools

After adding the links, open the Network panel in DevTools, filter by Images, and reload the page. You should see favicon.svg being requested instead of favicon.ico.

To test the dark variant without changing your OS theme: DevTools → Rendering tab → Emulate CSS media feature prefers-color-scheme → select dark. The tab icon updates immediately.

Step 4 — Add a Web App Manifest

A site.webmanifest file tells browsers how to treat your site when installed as a Progressive Web App (PWA), including which icons to use for the home screen launcher on Android and desktop.

{
  "name": "Your App Name",
  "short_name": "App",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "theme_color": "#ffffff",
  "background_color": "#ffffff",
  "display": "standalone"
}

Place this file at the root of your server and reference it with <link rel="manifest" href="/site.webmanifest">. Customize name, short_name, theme_color, and background_color to match your brand.

Progressive Enhancement

The beauty of this approach is that it degrades gracefully at every level. No user sees a broken or missing icon — each browser takes the best format it can display.

Browser / ContextWhat it gets
Modern Chromium / Firefoxfavicon.svg with embedded media query — fully adaptive
Browsers with PNG + media supportfavicon-32x32.png / favicon-dark-32x32.png via <link media="…">
Legacy browsersfavicon.ico with 16x16 and 32x32 embedded
iOS Safari home screenapple-touch-icon.png (180x180)
Android / PWA installerandroid-chrome-192x192.png and android-chrome-512x512.png via manifest

Complete File List

A fully-equipped favicon setup includes these files:

FilePurpose
favicon.icoLegacy multi-resolution icon (16x16, 32x32)
favicon.svgAdaptive SVG with prefers-color-scheme
favicon-16x16.pngSmall PNG for light mode
favicon-dark-16x16.pngSmall PNG for dark mode
favicon-32x32.pngStandard PNG for light mode
favicon-dark-32x32.pngStandard PNG for dark mode
apple-touch-icon.pngiOS home screen icon (180x180)
android-chrome-192x192.pngAndroid PWA icon
android-chrome-512x512.pngAndroid PWA icon (high-res)
site.webmanifestPWA manifest

Generate One Automatically

Building all of these files by hand — especially the binary .ico format, correctly-sized PNGs at every resolution, and a properly wired SVG wrapper — is tedious. Get Adaptive Favicon automates the entire pipeline in the browser:

  • From an image — Upload your own SVG, PNG, or JPG for both light and dark modes. The tool generates all sizes, writes the adaptive SVG wrapper, and packages everything into a ZIP.
  • From text — Choose any of 1,800+ Google Fonts, pick your colors and background shape (square, rounded, or circle), and get a fully rendered adaptive favicon without touching a design tool.
  • From an emoji — Pick any emoji in one of six artwork styles (Twemoji, OpenMoji, Blobmoji, Noto, Fluent, Fluent Flat) and turn it into a polished favicon set instantly.

Every generator outputs the same complete ZIP bundle: favicon.ico, favicon.svg, all PNG sizes, apple-touch-icon.png, PWA icons, site.webmanifest, and a ready-to-paste HTML snippet.

Conclusion

An adaptive favicon is a small but meaningful detail that makes your site feel crafted:

  1. Create favicon.svg with an embedded <style> block and a @media (prefers-color-scheme: dark) override.
  2. Keep favicon.ico as a fallback for legacy browsers.
  3. Add PNG variants with media attributes for additional compatibility.
  4. Include apple-touch-icon.png and a site.webmanifest for mobile and PWA support.
  5. Place all link tags in the correct order in your HTML head.

The whole setup requires zero JavaScript or build steps. If you want to skip the manual work, Get Adaptive Favicon generates the complete bundle for you in seconds.

Further Reading