如何创建浅色和深色模式自适应 Favicon:完整开发者指南

Favicon.im

现代网站需要适应用户偏好,而 favicon 主题化是一个经常被忽视的细节,却能显著提升用户体验。当用户在浅色和深色模式之间切换时,你的 favicon 应该相应适应以保持视觉一致性。

本指南涵盖从简单的纯 HTML 解决方案到各种流行框架的高级 JavaScript 实现。无论你是构建静态网站还是复杂的 Web 应用,都能找到适合你项目的方法。

方法一:纯 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>

方法二:JavaScript 实现

当你需要超越系统偏好的动态 favicon 切换时——例如自定义主题切换或实时更新——JavaScript 提供了你需要的灵活性。

使用 JavaScript 的场景:

  • 🎯 你有自定义主题控制
  • 🎯 你需要与应用的主题状态同步
  • 🎯 你想在不刷新页面的情况下更新 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;

方法三:框架集成

现代框架提供了处理 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>
    <!-- 你的应用内容 -->
  </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));
              }
            })();
          `
        }
      ]
    }
  }
})

方法四: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];

    // 清除画布
    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%
永久免费