起因
我的个人网站使用 Simple Icons CDN 渲染技能图标。为了适配暗色模式,我按照官方文档使用了双颜色 URL 格式:
<img src="https://cdn.simpleicons.org/nextdotjs/000000/ffffff" />
期望的行为是:亮色模式下图标为黑色(#000000),暗色模式下图标为白色(#ffffff)。
暗色模式确实正常工作,但当我手动将网站切换回亮色模式时,图标仍然是白色——在浅色背景上几乎不可见。
排查过程
第一步:验证 CDN 返回内容
用 curl 直接查看 CDN 返回的 SVG:
curl -s "https://cdn.simpleicons.org/nextdotjs/000000/ffffff"
返回的 SVG 内容:
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>Next.js</title>
<style>path{fill:#000000} @media (prefers-color-scheme:dark){path{fill:#ffffff}}</style>
<path d="M18.665 21.978C16.758..." />
</svg>
关键发现:CDN 的暗色模式适配使用的是 @media (prefers-color-scheme: dark) 媒体查询。
第二步:对比网站的主题实现
我的网站使用的是自定义暗色模式方案,核心逻辑是:
- 通过
localStorage存储用户偏好 - 通过
document.documentElement.classList.add('dark')切换主题 - CSS 变量通过
.dark选择器切换
而 <img> 标签加载的 SVG 是隔离文档,它无法感知宿主页面的 CSS 类或 DOM 状态。
第三步:定位冲突场景
问题出在操作系统暗色模式与网站主题不一致时:
| 操作系统 | 网站主题 | SVG 感知到的 | 实际填充色 | 结果 |
|---|---|---|---|---|
| 暗色 | 亮色 | prefers-color-scheme: dark | #ffffff | ❌ 白色图标在亮色背景上不可见 |
| 暗色 | 暗色 | prefers-color-scheme: dark | #ffffff | ✅ 正确 |
| 亮色 | 亮色 | prefers-color-scheme: light | #000000 | ✅ 正确 |
| 亮色 | 暗色 | prefers-color-scheme: light | #000000 | ❌ 黑色图标在暗色背景上不可见 |
根本原因:SVG 作为 <img> 标签加载时是隔离文档,只能感知 OS 级别的 prefers-color-scheme,无法感知网站的自定义主题。
解决方案
核心思路
既然 CDN 的 @media (prefers-color-scheme) 机制与自定义主题不兼容,那就绕过它——将双颜色 URL 拆分为单颜色 URL,由网站代码根据当前主题动态选择。
实现 getThemedIconUrl 函数
/**
* 根据网站主题转换 Simple Icons CDN URL
* CDN 的暗色模式使用 @media (prefers-color-scheme) 无法感知网站自定义主题
* 此函数将双颜色 URL 拆分为单颜色 URL,根据当前主题选择对应颜色
*
* @param url - 原始图标 URL
* @param theme - 当前网站主题
* @returns 适配当前主题的单颜色 URL
*/
function getThemedIconUrl(url: string, theme: 'light' | 'dark'): string {
if (!url.includes('cdn.simpleicons.org')) return url;
try {
const urlObj = new URL(url);
const parts = urlObj.pathname.split('/').filter(Boolean);
if (parts.length < 3) return url;
const slug = parts[0];
const lightColor = parts[1];
const darkColor = parts[2];
const color = theme === 'dark' ? darkColor : lightColor;
urlObj.pathname = color === '_' ? `/${slug}` : `/${slug}/${color}`;
return urlObj.toString();
} catch {
return url;
}
}
URL 转换示例
| 原始 URL | 亮色模式下 | 暗色模式下 |
|---|---|---|
/nextdotjs/000000/ffffff | /nextdotjs/000000 | /nextdotjs/ffffff |
/github/_/ffffff | /github(默认品牌色) | /github/ffffff |
/react/61dafb/ffffff | /react/61dafb | /react/ffffff |
其中 _ 是 Simple Icons CDN 的特殊占位符,表示"使用默认品牌色"。
在组件中使用
function SkillCard({ skill, colorKey, index }: SkillCardProps) {
const { theme } = useTheme();
const hasCustomIcon = skill.icon && /^https?:\/\//.test(skill.icon);
const iconSrc = hasCustomIcon && skill.icon
? getThemedIconUrl(skill.icon, theme)
: undefined;
return (
<div className="...">
{iconSrc ? (
<img
key={iconSrc}
src={iconSrc}
alt={skill.name}
className="h-4 w-4 object-contain animate-icon-theme-fade"
/>
) : (
skill.name.charAt(0).toUpperCase()
)}
</div>
);
}
注意 key={iconSrc} 的使用——当主题切换导致 URL 变化时,React 会卸载旧元素、挂载新元素,从而触发淡入动画。
缓存分析
你可能会担心:切换主题时每次都要重新请求 CDN 吗?不需要。因为亮色和暗色使用的是不同的 URL,浏览器会为每个 URL 维护独立的缓存条目。
验证 CDN 的缓存策略:
curl -sI "https://cdn.simpleicons.org/nextdotjs/000000" | grep -i cache
Cache-Control: public, max-age=86400, s-maxage=31536000, stale-while-revalidate=604800
cf-cache-status: HIT
| 缓存参数 | 值 | 含义 |
|---|---|---|
max-age | 86400 | 浏览器本地缓存 24 小时 |
s-maxage | 31536000 | CDN 边缘节点缓存 1 年 |
stale-while-revalidate | 604800 | 过期后 7 天内可先返回旧资源再后台刷新 |
实际体验:首次加载时两种颜色各请求一次,之后所有切换都是浏览器本地缓存命中,零延迟、零网络开销。
淡入动画
切换主题时图标颜色瞬间变化有些生硬,加了一个 200ms 的淡入动画让过渡更自然。
CSS 定义:
@keyframes icon-theme-fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
在 Tailwind v4 的 @theme inline 中注册:
@theme inline {
--animate-icon-theme-fade: icon-theme-fade 0.2s ease-in-out;
}
组件中使用 animate-icon-theme-fade 类即可。配合 key={iconSrc},每次主题切换都会触发动画重新播放。
其他方案的对比
调研过程中我还考虑了其他方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 动态 URL(最终方案) | 完全控制、缓存友好、支持任意颜色 | 需要修改组件代码 |
CSS dark:invert | 零代码改动,一行 CSS | 只适合黑白图标,彩色图标颜色会反转 |
CSS color-scheme 属性 | 保留 CDN 双颜色 URL | 浏览器支持不一致,<img> 内部 SVG 行为不可靠 |
内联 SVG + currentColor | 最灵活 | 需要本地存储所有 SVG,失去 CDN 的便利性 |
总结
当网站使用自定义暗色模式(非 prefers-color-scheme)时,Simple Icons CDN 的双颜色 URL 会因为 SVG 隔离文档的限制而失效。解决方案是将双颜色 URL 拆分为单颜色 URL,由网站代码根据当前主题动态选择,配合 key 属性触发 React 重挂载和淡入动画,实现流畅的主题切换体验。
核心要点:
<img>标签内的 SVG 是隔离文档,无法感知宿主页面的 CSS 类- Simple Icons CDN 的暗色模式依赖
prefers-color-scheme,与自定义主题系统不兼容 - 拆分为单颜色 URL + 动态选择是可靠且缓存友好的方案
key属性是触发 React 重挂载动画的关键技巧