这个博客的昼夜切换,从日出橙到深林绿,背后只有一个 HTML 属性、一组 CSS 变量、和几行 JavaScript。

没有框架,没有状态管理。切换一个属性,颜色就跟着变。

核心思路

data-theme 属性作为开关,挂在 <html> 元素上:

:root[data-theme="day"] {
  --bg:   #f4ecd8;
  --ink:  #4a3f2f;
  --moss: #7a8c4a;
  --gold: #d4a943;
}

:root[data-theme="night"] {
  --bg:   #1a2118;
  --ink:  #e8e4d4;
  --moss: #8fa85a;
  --gold: #f0c95a;
}

然后整个页面的所有颜色都用 var(--ink)var(--bg) 这类变量写。切换主题时,只需要改 data-theme 的值,CSS 变量会级联更新到所有地方。

过渡动画

光有变量还不够,直接切换会闪烁。加一条 transition 让它呼吸:

body {
  background: var(--bg);
  color: var(--ink);
  transition:
    background 1.1s cubic-bezier(.4, 0, .2, 1),
    color 1.1s ease;
}

注意曲线选的是缓入缓出,不是线性。颜色变化本身就是视觉信息,一点缓冲让眼睛有时间适应。1.1 秒是我测试后觉得最"像呼吸"的时长——再快变成切换,再慢变成等待。

JavaScript 部分

const root = document.documentElement;

toggle.addEventListener('click', () => {
  const theme = root.getAttribute('data-theme');
  root.setAttribute('data-theme', theme === 'day' ? 'night' : 'day');
});

就这几行。不需要 localStorage(这是个阅读型页面,不是应用),不需要监听系统偏好(主题是读者主动选的)。

为什么不用 CSS class

很多实现用 .dark-mode class 切换,理论上也可以。我用 data-theme 是因为它更语义化——属性名就说清楚了这是什么。而且以后想加第三个主题(比如"黄昏")也是自然扩展:

:root[data-theme="dusk"] {
  --bg:   #2d1f1a;
  --ink:  #f0e0c8;
  --moss: #c8906a;
}

而不是 .dark-mode.dusk-mode 这种奇怪的组合。


大部分视觉系统的复杂度,都可以用"对的抽象层"压下去。这里的对的抽象层,是 CSS 自定义属性。