如何建立淺色與深色模式自適應 Favicon:完整開發者指南
現代網站需要適應使用者偏好,而 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)更平衡
- 考慮細微的陰影或輪廓以定義邊緣
設計一致性技巧
- 維持品牌識別 - 保持核心設計元素一致
- 在多種尺寸測試 - 16x16、32x32 和 180x180 像素
- 使用簡單形狀 - 複雜細節在小尺寸下會消失
- 考慮色盲使用者 - 不要僅依賴顏色區分
檔案命名慣例
用清晰的命名組織您的 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 都維持不變
常見原因與解決方案:
-
瀏覽器快取問題
<!-- 加入快取清除參數 --> <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)"> -
媒體查詢語法不正確
<!-- 錯誤 --> <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.im 快速檢查您的 favicon 是否配置正確。我們的免費工具確保您網站的 favicon 在所有瀏覽器和裝置上正確顯示。
免費公共服務
Favicon.im 是一個完全免費的公共服務,受到全球開發者的信賴。