主题只有两个。
亮色。
深色。
一个 Boolean 就能解决:
const dark = true;完工。
然后有人问:“能不能跟随系统?”
于是现在有三个:
system
light
dark这个 Boolean 的职业生涯只有四分钟。
最简单的深色模式其实不需要 JavaScript
@media (prefers-color-scheme: dark) {
body {
background: #111;
color: #eee;
}
}浏览器会读取系统的颜色偏好。手机深色,网页深色;系统亮色,网页亮色。
直到你加上“手动切换主题”。
用户选择应该压过系统选择
设备处于深色,但用户偏偏想让这个网站保持亮色。刷新后也应该继续亮色。因此你需要记住网站自己的选择,比如:
localStorage.setItem("theme", "light");逻辑变成:
- 用户有没有显式选择主题?有,就用它。
- 没有,就读取系统。
所以数据模型最好从一开始就是 system / light / dark,而不是硬塞进 true / false。
然后你获得了“闪白一下”
主题切换完成了,刷新页面却先亮白半秒,再变成深色。
这不是灵异,是加载顺序:
HTML 开始渲染
↓
默认亮色先出现
↓
JavaScript 加载
↓
读取 localStorage
↓
切到深色浏览器完全按你的代码工作,只是用户的眼睛也看到了这个时间线。
解决思路是尽量在页面第一次绘制前就确定主题,用一小段早期脚本读取保存值和 prefers-color-scheme,给 html 设置主题属性。
CSS 变量比复制整套样式靠谱
:root {
--bg: white;
--text: #111;
--surface: #f4f4f4;
}
html[data-theme="dark"] {
--bg: #111;
--text: #eee;
--surface: #1c1c1c;
}组件只需要使用 var(--surface),不用知道深色模式内部怎么实现。以后加主题也不会到处找硬编码颜色。
网页深了,浏览器栏还亮着
移动浏览器的地址栏、状态区域和 PWA 外壳也可能受 theme-color 影响。如果网页切成深色,而浏览器顶部仍然白得发光,整体就像额头贴了一盏手电筒。
动态主题最好同步更新对应的 meta name="theme-color"。
原生表单控件也要通知
color-scheme 可以让浏览器知道页面支持哪些主题,帮助输入框、滚动条等原生元素匹配。否则你精心做好的深色界面里,可能孤零零站着一个雪白的系统控件提醒你:“漏了我。”
System 不是“第一次加载时读一下系统”
如果用户选择跟随系统,晚上操作系统从亮色自动切深色,网页也应该跟着改变。可以监听:
matchMedia("(prefers-color-scheme: dark)")只有当前模式是 system 时,才响应变化。否则用户明明选了 light,你不能在晚上擅自替他改回 dark。
深色模式不是“背景黑一点”
还要检查文字对比度、次要文字、禁用状态、边框、链接、焦点框、错误提示和图标。低对比灰字在设计稿里可能很“高级”,到了普通屏幕和户外环境里就变成寻找彩蛋。
最后
深色模式一开始只是换颜色。
然后变成状态管理。
再变成本地持久化。
再处理首屏闪烁、系统变化、浏览器 UI 和可访问性。
一枚月亮图标,职业发展相当丰富。
最干净的模型通常仍然是三个状态:
system
light
dark尊重用户显式选择,让 system 真正跟随系统,并尽早应用首屏主题。
这样深色模式才不只是“网页变黑”,而是真的像一个完整功能。