返回书卷列表
Notes

博客暗色主题的实现:CSS 变量与三种切换策略

2 分钟阅读

暗色主题不是换个背景色——变量分层、跟随系统与手动切换的持久化、以及图片和代码块的暗色适配

#css#dark-mode#theme#frontend

博客暗色主题的实现:CSS 变量与三种切换策略

暗色主题最常见的失败形态:背景是暗的,但卡片还是白的、代码块刺眼、图片边缘生硬。这些问题的根源是同一个——把「暗色主题」当成了「换背景色」,而它实际是「整套语义色板的第二份定义」。

变量分层:原始色与语义色

:root {
  --gray-9: oklch(0.98 0 0);    /* 原始色板 */
  --accent: oklch(0.7 0.15 250);
  /* 语义层:组件只消费这一层 */
  --background: var(--gray-9);
  --card: white;
}
[data-theme='dark'] {
  --background: oklch(0.18 0 0);
  --card: oklch(0.23 0 0);
}

组件样式只写 bg-card,永远不直接写具体色值。暗色适配的工作量被压缩成「重新定义语义层」一件事。没有分层的项目,暗色模式意味着在每个组件里各写一遍 dark: 前缀——那是体力活加漏网之鱼的配方。

三种切换策略,按优先级组合

  1. 跟随系统:@media (prefers-color-scheme: dark) 里定义暗色变量。零 JS,尊重用户系统设置,是默认态。
  2. 手动覆盖:用户点击切换按钮时在 <html> 上设 data-theme,变量定义从 media query 移到属性选择器。适合系统设置与个人偏好不一致的用户。
  3. 持久化:localStorage 存用户选择,页面加载前用一段内联脚本读取并设置 data-theme。

内联脚本的时机是关键:等 React 水合再设置会出现「先白后暗」的闪烁(FOUC)。正确位置是 <head> 里的一段同步脚本,几行代码,在首帧绘制前完成。

<script>
  const t = localStorage.getItem('theme');
  if (t) document.documentElement.dataset.theme = t;
</script>

容易漏掉的四类元素

  • 图片:亮色底图在暗色页面里像贴了白膏药。降饱和滤镜(filter: brightness(.85))是廉价而有效的方案。
  • 代码块:Shiki 支持双主题输出,CSS 按主题切换 --shiki-dark 变量即可,不要在运行时重新高亮。
  • 阴影:暗色底上黑色阴影几乎不可见,暗色模式的「深度感」要靠表面亮度差(--card 比 --background 亮一档)来传达。
  • 半透明遮罩:rgba(0,0,0,.5) 的弹窗遮罩在暗色下让层级关系消失,暗色主题里遮罩应该用半透明白或更深的黑加模糊。

测试清单

暗色模式上线前过一遍:首屏无闪烁(内联脚本生效)、所有页面级对比度达标(WCAG AA)、代码块双主题正确、图片不刺眼、弹窗与层级清晰、系统切换时无手动覆盖的页面实时跟随。最后一项最容易漏——跟随系统意味着用户可能开着页面时切系统主题,CSS 变量方案天然支持这个,写死色值的方案当场露馅。

一个反模式

不要用 filter: invert(1) 整页反色做暗色模式。图片会变负片、品牌色会失真、阴影逻辑全乱。它看起来像捷径,实际上把所有问题延迟到了更难修的地方。

基于站内文章回答,并标注引用来源

留言

加载中…

0/500