如何建立淺色與深色模式自適應 Favicon:完整開發者指南

Favicon.im

現代網站需要適應使用者偏好,而 favicon 主題化是一個經常被忽略但能顯著提升使用者體驗的細節。當使用者在淺色和深色模式之間切換時,您的 favicon 應該相應適配以維持視覺一致性。

這份完整指南涵蓋從簡單的純 HTML 解決方案到熱門框架的進階 JavaScript 實作。無論您是建立靜態網站還是複雜的 Web 應用程式,都能找到適合您專案的方法。

方法 1:純 HTML 解決方案(大多數網站推薦)

純 HTML 方法是最可靠的方法,不需要 JavaScript。它使用 favicon link 標籤的 media 屬性中的 CSS 媒體查詢,根據使用者的系統偏好自動切換 favicon。

為什麼這個方法最有效:

  • 零 JavaScript 需求
  • 頁面載入時立即運作
  • 所有現代瀏覽器支援
  • 無效能開銷

基本實作

<head>
  <!-- 預設 favicon(不支援的瀏覽器備用) -->
  <link rel="icon" href="/favicon-light.ico" type="image/x-icon">

  <!-- 淺色模式 favicon -->
  <link rel="icon" href="/favicon-light.ico" type="image/x-icon" media="(prefers-color-scheme: light)">

  <!-- 深色模式 favicon -->
  <link rel="icon" href="/favicon-dark.ico" type="image/x-icon" media="(prefers-color-scheme: dark)">
</head>

完整多尺寸實作

要獲得完整的裝置支援,實作多種尺寸的主題變體:

<head>
  <!-- 預設 favicon(備用) -->
  <link rel="icon" type="image/x-icon" href="/favicon-light.ico">
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png">

  <!-- 淺色模式 favicon -->
  <link rel="icon" type="image/x-icon" href="/favicon-light.ico" media="(prefers-color-scheme: light)">
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-light-16x16.png" media="(prefers-color-scheme: light)">
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png" media="(prefers-color-scheme: light)">
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-light.png" media="(prefers-color-scheme: light)">

  <!-- 深色模式 favicon -->
  <link rel="icon" type="image/x-icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon-dark-16x16.png" media="(prefers-color-scheme: dark)">
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon-dark-32x32.png" media="(prefers-color-scheme: dark)">
  <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-dark.png" media="(prefers-color-scheme: dark)">

  <!-- 帶有嵌入 CSS 的 SVG favicon -->
  <link rel="icon" type="image/svg+xml" href="/favicon-adaptive.svg">
</head>

自適應 SVG Favicon

建立單一 SVG favicon 自動適應配色方案:

<!-- favicon-adaptive.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    .light-mode { fill: #000000; }
    .dark-mode { fill: #ffffff; }

    @media (prefers-color-scheme: dark) {
      .light-mode { display: none; }
    }

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

  <!-- 淺色模式設計 -->
  <circle class="light-mode" cx="16" cy="16" r="12"/>
  <text class="light-mode" x="16" y="20" text-anchor="middle" fill="#fff" font-size="14">L</text>

  <!-- 深色模式設計 -->
  <circle class="dark-mode" cx="16" cy="16" r="12"/>
  <text class="dark-mode" x="16" y="20" text-anchor="middle" fill="#000" font-size="14">D</text>
</svg>

方法 2:JavaScript 實作

當您需要超越系統偏好的動態 favicon 切換時——例如自訂主題切換或即時更新——JavaScript 提供您需要的靈活性。

使用 JavaScript 的時機:

  • 您有自訂主題控制
  • 您需要與 app 的主題狀態同步
  • 您想要不重新整理頁面更新 favicon
  • 您正在建立單頁應用程式

基本 JavaScript 方法

// 根據主題更新 favicon 的函數
function updateFavicon(theme) {
  const favicon = document.querySelector('link[rel="icon"]') ||
                 document.createElement('link');

  favicon.rel = 'icon';
  favicon.type = 'image/png';
  favicon.href = theme === 'dark' ? '/favicon-dark.png' : '/favicon-light.png';

  if (!document.querySelector('link[rel="icon"]')) {
    document.head.appendChild(favicon);
  }
}

// 監聽系統主題變化
if (window.matchMedia) {
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

  // 設定初始 favicon
  updateFavicon(mediaQuery.matches ? 'dark' : 'light');

  // 監聽變化
  mediaQuery.addEventListener('change', (e) => {
    updateFavicon(e.matches ? 'dark' : 'light');
  });
}

進階 JavaScript 支援多尺寸

class FaviconManager {
  constructor() {
    this.sizes = [
      { size: '16x16', selector: 'link[rel="icon"][sizes="16x16"]' },
      { size: '32x32', selector: 'link[rel="icon"][sizes="32x32"]' },
      { size: '180x180', selector: 'link[rel="apple-touch-icon"]' }
    ];

    this.init();
  }

  init() {
    // 設定初始主題
    this.updateTheme(this.getSystemTheme());

    // 監聽系統變化
    if (window.matchMedia) {
      const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
      mediaQuery.addEventListener('change', (e) => {
        this.updateTheme(e.matches ? 'dark' : 'light');
      });
    }
  }

  getSystemTheme() {
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark' : 'light';
  }

  updateTheme(theme) {
    this.sizes.forEach(({ size, selector }) => {
      let link = document.querySelector(selector);

      if (!link) {
        link = document.createElement('link');
        link.rel = size === '180x180' ? 'apple-touch-icon' : 'icon';
        link.type = 'image/png';
        if (size !== '180x180') link.sizes = size;
        document.head.appendChild(link);
      }

      link.href = `/favicon-${theme}-${size}.png`;
    });

    // 更新預設 ico 檔案
    let icoLink = document.querySelector('link[rel="icon"][type="image/x-icon"]');
    if (!icoLink) {
      icoLink = document.createElement('link');
      icoLink.rel = 'icon';
      icoLink.type = 'image/x-icon';
      document.head.appendChild(icoLink);
    }
    icoLink.href = `/favicon-${theme}.ico`;
  }

  // 手動設定主題的方法(用於自訂主題切換)
  setTheme(theme) {
    this.updateTheme(theme);
  }
}

// 初始化
const faviconManager = new FaviconManager();

// 匯出以供手動主題切換
window.faviconManager = faviconManager;

方法 3:框架整合

現代框架提供優雅的方式處理 favicon 主題化。以下是如何在最流行的 JavaScript 框架中實作自適應 favicon。

React 實作

import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';

function AdaptiveFavicon() {
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    // 檢查系統偏好
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    setTheme(mediaQuery.matches ? 'dark' : 'light');

    // 監聽變化
    const handleChange = (e) => {
      setTheme(e.matches ? 'dark' : 'light');
    };

    mediaQuery.addEventListener('change', handleChange);
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  return (
    <Helmet>
      <link rel="icon" type="image/x-icon" href={`/favicon-${theme}.ico`} />
      <link rel="icon" type="image/png" sizes="32x32" href={`/favicon-${theme}-32x32.png`} />
      <link rel="apple-touch-icon" sizes="180x180" href={`/apple-touch-icon-${theme}.png`} />
    </Helmet>
  );
}

Vue 3 實作

<template>
  <div>
    <!-- 您的 app 內容 -->
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { useHead } from '@unhead/vue'

const isDark = ref(false)

const updateFavicon = () => {
  const theme = isDark.value ? 'dark' : 'light'

  useHead({
    link: [
      { rel: 'icon', type: 'image/x-icon', href: `/favicon-${theme}.ico` },
      { rel: 'icon', type: 'image/png', sizes: '32x32', href: `/favicon-${theme}-32x32.png` },
      { rel: 'apple-touch-icon', sizes: '180x180', href: `/apple-touch-icon-${theme}.png` }
    ]
  })
}

onMounted(() => {
  // 檢查系統偏好
  if (window.matchMedia) {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    isDark.value = mediaQuery.matches

    // 監聽變化
    mediaQuery.addEventListener('change', (e) => {
      isDark.value = e.matches
    })
  }

  updateFavicon()
})

watch(isDark, updateFavicon)
</script>

Nuxt 3 實作

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          innerHTML: `
            (function() {
              const updateFavicon = (isDark) => {
                const theme = isDark ? 'dark' : 'light';
                const links = [
                  { rel: 'icon', type: 'image/x-icon', href: \`/favicon-\${theme}.ico\` },
                  { rel: 'icon', type: 'image/png', sizes: '32x32', href: \`/favicon-\${theme}-32x32.png\` }
                ];

                links.forEach(linkData => {
                  let link = document.querySelector(\`link[rel="\${linkData.rel}"][sizes="\${linkData.sizes || 'any'}"]\`);
                  if (!link) {
                    link = document.createElement('link');
                    Object.assign(link, linkData);
                    document.head.appendChild(link);
                  } else {
                    link.href = linkData.href;
                  }
                });
              };

              if (window.matchMedia) {
                const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
                updateFavicon(mediaQuery.matches);
                mediaQuery.addEventListener('change', e => updateFavicon(e.matches));
              }
            })();
          `
        }
      ]
    }
  }
})

方法 4:CSS-in-JS Favicon(進階)

使用 Canvas 和 CSS 顏色動態產生 favicon:

class DynamicFaviconGenerator {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.canvas.width = 32;
    this.canvas.height = 32;
  }

  generateFavicon(theme) {
    const colors = {
      light: { bg: '#ffffff', text: '#000000' },
      dark: { bg: '#000000', text: '#ffffff' }
    };

    const { bg, text } = colors[theme];

    // 清除 canvas
    this.ctx.clearRect(0, 0, 32, 32);

    // 繪製背景
    this.ctx.fillStyle = bg;
    this.ctx.fillRect(0, 0, 32, 32);

    // 繪製邊框
    this.ctx.strokeStyle = text;
    this.ctx.lineWidth = 2;
    this.ctx.strokeRect(2, 2, 28, 28);

    // 繪製圖示(範例:字母或符號)
    this.ctx.fillStyle = text;
    this.ctx.font = 'bold 20px Arial';
    this.ctx.textAlign = 'center';
    this.ctx.textBaseline = 'middle';
    this.ctx.fillText('F', 16, 16);

    return this.canvas.toDataURL('image/png');
  }

  updateFavicon(theme) {
    const dataUrl = this.generateFavicon(theme);

    let link = document.querySelector('link[rel="icon"]');
    if (!link) {
      link = document.createElement('link');
      link.rel = 'icon';
      link.type = 'image/png';
      document.head.appendChild(link);
    }

    link.href = dataUrl;
  }
}

// 使用方式
const generator = new DynamicFaviconGenerator();
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

generator.updateFavicon(mediaQuery.matches ? 'dark' : 'light');
mediaQuery.addEventListener('change', e => {
  generator.updateFavicon(e.matches ? 'dark' : 'light');
});

設計最佳實務

建立有效的自適應 favicon 需要仔細關注設計原則和使用者體驗。

色彩對比與可見度

淺色模式 Favicon 設計:

  • 在透明或淺色背景上使用深色元素(文字、圖示)
  • 目標 WCAG AA 對比度(最少 4.5:1)
  • 在白色瀏覽器分頁和書籤列上測試外觀
  • 確保在 16x16 像素(最常見的小尺寸)下清晰

深色模式 Favicon 設計:

  • 在透明或深色背景上使用淺色元素
  • 在深色瀏覽器主題下測試可見度
  • 避免純白(#ffffff)——使用灰白色(#f0f0f0)更平衡
  • 考慮細微的陰影或輪廓以定義邊緣

設計一致性技巧

  1. 維持品牌識別 - 保持核心設計元素一致
  2. 在多種尺寸測試 - 16x16、32x32 和 180x180 像素
  3. 使用簡單形狀 - 複雜細節在小尺寸下會消失
  4. 考慮色盲使用者 - 不要僅依賴顏色區分

檔案命名慣例

用清晰的命名組織您的 favicon 檔案:

/public/
├── favicon-light.ico
├── favicon-dark.ico
├── favicon-light-16x16.png
├── favicon-dark-16x16.png
├── favicon-light-32x32.png
├── favicon-dark-32x32.png
├── apple-touch-icon-light.png
├── apple-touch-icon-dark.png
└── favicon-adaptive.svg

瀏覽器相容性

現代瀏覽器對自適應 Favicon 的支援

瀏覽器 媒體查詢支援 備註
Chrome 76+ 完整支援 完美運作
Firefox 67+ 完整支援 優秀實作
Safari 12.1+ 完整支援 包含 iOS Safari
Edge 79+ 完整支援 Chromium 版 Edge
Internet Explorer 不支援 使用 JavaScript 備用

市場覆蓋率: 這些版本截至 2025 年涵蓋約 95% 的全球瀏覽器使用量。

備用策略

<!-- 始終提供備用 -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon">

<!-- 現代瀏覽器增強支援 -->
<link rel="icon" href="/favicon-light.ico" type="image/x-icon" media="(prefers-color-scheme: light)">
<link rel="icon" href="/favicon-dark.ico" type="image/x-icon" media="(prefers-color-scheme: dark)">

<!-- 舊瀏覽器的 JavaScript 備用 -->
<script>
  if (!window.matchMedia || !CSS.supports('(prefers-color-scheme: dark)')) {
    // 根據時間或其他啟發式方法載入 favicon
    const hour = new Date().getHours();
    const isDark = hour < 6 || hour > 18;
    document.querySelector('link[rel="icon"]').href =
      isDark ? '/favicon-dark.ico' : '/favicon-light.ico';
  }
</script>

測試與驗證

手動測試檢查清單

  • [ ] 在淺色模式測試(系統偏好)
  • [ ] 在深色模式測試(系統偏好)
  • [ ] 驗證系統主題切換時 favicon 立即變化
  • [ ] 檢查不同瀏覽器(Chrome、Firefox、Safari、Edge)
  • [ ] 在行動裝置上測試
  • [ ] 驗證舊瀏覽器的備用行為

自動化測試

// favicon 主題切換測試腳本
function testFaviconThemes() {
  const tests = [
    { theme: 'light', expected: '/favicon-light.ico' },
    { theme: 'dark', expected: '/favicon-dark.ico' }
  ];

  tests.forEach(({ theme, expected }) => {
    // 模擬媒體查詢
    Object.defineProperty(window, 'matchMedia', {
      writable: true,
      value: jest.fn().mockImplementation(query => ({
        matches: query.includes('dark') ? theme === 'dark' : theme === 'light',
        addEventListener: jest.fn(),
        removeEventListener: jest.fn(),
      })),
    });

    // 觸發更新
    updateFavicon(theme);

    // 斷言
    const favicon = document.querySelector('link[rel="icon"]');
    expect(favicon.href).toContain(expected);
  });
}

效能最佳化

預載主題 Favicon

<!-- 預載兩種主題 favicon 以獲得即時切換 -->
<link rel="preload" as="image" href="/favicon-light.ico">
<link rel="preload" as="image" href="/favicon-dark.ico">

最小化檔案大小

  • 保持 ICO 檔案在 1KB 以下
  • 使用 TinyPNG 等工具最佳化 PNG 檔案
  • 簡單幾何設計使用 SVG
  • 現代瀏覽器考慮 WebP 格式

快取策略

# Nginx favicon 快取配置
location ~* \.(ico|png|svg)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    add_header Vary "Accept-Encoding";
}

常見問題排解

Favicon 未在主題間切換

症狀: 無論系統主題變化 favicon 都維持不變

常見原因與解決方案:

  1. 瀏覽器快取問題

    <!-- 加入快取清除參數 -->
    <link rel="icon" href="/favicon-light.ico?v=2025" media="(prefers-color-scheme: light)">
    <link rel="icon" href="/favicon-dark.ico?v=2025" media="(prefers-color-scheme: dark)">
    
  2. 媒體查詢語法不正確

    <!-- 錯誤 -->
    <link rel="icon" href="/favicon-dark.ico" media="dark">
    
    <!-- 正確 -->
    <link rel="icon" href="/favicon-dark.ico" media="(prefers-color-scheme: dark)">
    

多個 Favicon 同時載入

症狀: Network 分頁顯示多個 favicon 請求

解決方案: 使用 JavaScript 取代而非加入:

function replaceFavicon(href) {
  // 移除所有現有的 favicon 連結
  document.querySelectorAll('link[rel*="icon"]').forEach(link => link.remove());

  // 加入新 favicon
  const link = document.createElement('link');
  link.rel = 'icon';
  link.type = 'image/x-icon';
  link.href = href;
  document.head.appendChild(link);
}

SVG Favicon 未顯示

症狀: SVG favicon 在某些瀏覽器有效但其他無效

根本原因: 舊瀏覽器對 SVG favicon 支援有限

解決方案: 始終提供 PNG 備用:

<!-- 現代瀏覽器:帶媒體查詢的 SVG -->
<link rel="icon" type="image/svg+xml" href="/favicon-adaptive.svg">

<!-- 備用:舊瀏覽器使用 PNG -->
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-light-32x32.png" media="(prefers-color-scheme: light)">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-dark-32x32.png" media="(prefers-color-scheme: dark)">

進階技術

主題感知通知徽章

class NotificationFavicon {
  constructor() {
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.canvas.width = 32;
    this.canvas.height = 32;
    this.baseIcons = {
      light: '/favicon-light-32x32.png',
      dark: '/favicon-dark-32x32.png'
    };
  }

  async drawWithBadge(theme, count) {
    const baseIcon = new Image();
    baseIcon.src = this.baseIcons[theme];

    return new Promise(resolve => {
      baseIcon.onload = () => {
        this.ctx.clearRect(0, 0, 32, 32);
        this.ctx.drawImage(baseIcon, 0, 0, 32, 32);

        if (count > 0) {
          // 繪製通知徽章
          const badgeSize = 12;
          const x = 32 - badgeSize;
          const y = 0;

          // 徽章背景
          this.ctx.fillStyle = '#ff4444';
          this.ctx.beginPath();
          this.ctx.arc(x + badgeSize/2, y + badgeSize/2, badgeSize/2, 0, 2 * Math.PI);
          this.ctx.fill();

          // 徽章文字
          this.ctx.fillStyle = 'white';
          this.ctx.font = '8px Arial';
          this.ctx.textAlign = 'center';
          this.ctx.textBaseline = 'middle';
          this.ctx.fillText(
            count > 9 ? '9+' : count.toString(),
            x + badgeSize/2,
            y + badgeSize/2
          );
        }

        resolve(this.canvas.toDataURL());
      };
    });
  }

  async updateWithNotification(count = 0) {
    const theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    const dataUrl = await this.drawWithBadge(theme, count);

    let link = document.querySelector('link[rel="icon"]');
    if (!link) {
      link = document.createElement('link');
      link.rel = 'icon';
      document.head.appendChild(link);
    }
    link.href = dataUrl;
  }
}

// 使用方式
const notificationFavicon = new NotificationFavicon();
notificationFavicon.updateWithNotification(3); // 顯示數量 3 的徽章

總結與後續步驟

自適應 favicon 是增強使用者體驗的一個小但有影響力的方式。它們展示了對細節的關注和對使用者偏好的尊重,為更加精緻和專業的網站做出貢獻。

為您的專案選擇正確的方法

方法 最適合 複雜度 效能
純 HTML 靜態網站、部落格、行銷頁面 優秀
JavaScript SPA、自訂主題、動態更新 良好
框架整合 React/Vue/Nuxt 應用程式 良好
進階技術 通知系統、即時更新 視情況而定

實作檢查清單

在部署您的自適應 favicon 系統之前:

  • [ ] 建立淺色和深色 favicon 版本
  • [ ] 在多個瀏覽器測試(Chrome、Firefox、Safari、Edge)
  • [ ] 驗證系統主題變化時切換正常
  • [ ] 在行動裝置測試(iOS Safari、Android Chrome)
  • [ ] 最佳化檔案大小(ICO 檔案保持在 1KB 以下)
  • [ ] 為舊瀏覽器加入適當備用
  • [ ] 使用 Favicon.im 等工具驗證實作

效能影響

正確實作時,自適應 favicon 的效能影響極小:

  • 純 HTML 方法:零 JavaScript 開銷
  • 檔案大小影響:總計約 2-4KB(淺色 + 深色版本)
  • 載入時間:正確快取時可忽略

進一步發展

考慮這些進階最佳化:

  • 預載關鍵 favicon 素材以獲得即時切換
  • 使用 WebP 格式用於現代瀏覽器(配合 PNG 備用)
  • 實作動態 favicon 徽章用於通知
  • 加入 favicon 動畫用於特殊事件或狀態

透過深思熟慮地實作自適應 favicon,您可以創造更加一致和使用者友善的 Web 體驗,適應現代使用者偏好。

檢查您的 Favicon

使用 favicon.im 快速檢查您的 favicon 是否配置正確。我們的免費工具確保您網站的 favicon 在所有瀏覽器和裝置上正確顯示。

免費公共服務

Favicon.im 是一個完全免費的公共服務,受到全球開發者的信賴。

15M+
每月 Favicon 請求數
100%
永久免費