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: 前缀——那是体力活加漏网之鱼的配方。
三种切换策略,按优先级组合
- 跟随系统:
@media (prefers-color-scheme: dark)里定义暗色变量。零 JS,尊重用户系统设置,是默认态。 - 手动覆盖:用户点击切换按钮时在
<html>上设data-theme,变量定义从 media query 移到属性选择器。适合系统设置与个人偏好不一致的用户。 - 持久化:
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) 整页反色做暗色模式。图片会变负片、品牌色会失真、阴影逻辑全乱。它看起来像捷径,实际上把所有问题延迟到了更难修的地方。
基于站内文章回答,并标注引用来源
留言
加载中…